"""Step definitions for repository error-handling coverage.

Targets uncovered ``except (OperationalError, SQLAlchemyDatabaseError)`` and
``except IntegrityError`` handlers across all major repository classes:

- NamespacedProjectRepository
- ToolRepository
- AutomationProfileRepository
- ResourceTypeRepository
- ResourceRepository
- ProjectResourceLinkRepository
- ValidationAttachmentRepository
- LifecyclePlanRepository

Strategy: replace the session factory with one returning a mock session whose
``.query()`` or ``.flush()`` raises ``OperationalError`` / ``IntegrityError``,
forcing every error handler to execute.  The ``@database_retry`` decorator
retries 3 times on ``DatabaseError`` before re-raising.
"""

from __future__ import annotations

from datetime import UTC, datetime
from types import SimpleNamespace
from unittest.mock import MagicMock

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

from cleveragents.core.exceptions import DatabaseError
from cleveragents.infrastructure.database.models import Base
from cleveragents.infrastructure.database.repositories import (
    AutomationProfileRepository,
    DuplicateResourceError,
    DuplicateToolError,
    DuplicateValidationAttachmentError,
    LifecyclePlanRepository,
    NamespacedProjectRepository,
    ProjectResourceLinkRepository,
    ResourceRepository,
    ResourceTypeRepository,
    ToolNotFoundError,
    ToolRepository,
    ValidationAttachmentRepository,
)

# ---------------------------------------------------------------------------
# Mock session helpers
# ---------------------------------------------------------------------------


def _mock_session_op_error_on_query() -> MagicMock:
    """Session whose .query() always raises OperationalError."""
    mock = MagicMock()
    mock.query.side_effect = OperationalError("connection lost", {}, None)
    mock.rollback.return_value = None
    return mock


def _mock_session_op_error_on_flush() -> MagicMock:
    """Session whose .flush()/.commit() raises OperationalError but .add()/.query() work."""
    mock = MagicMock()
    mock.add.return_value = None
    mock.flush.side_effect = OperationalError("disk I/O error", {}, None)
    mock.commit.side_effect = OperationalError("disk I/O error", {}, None)
    mock.rollback.return_value = None
    # query returns a mock that allows filter_by().first() to return None
    query_mock = MagicMock()
    query_mock.filter_by.return_value.first.return_value = None
    query_mock.filter.return_value.first.return_value = None
    mock.query.return_value = query_mock
    return mock


def _mock_session_unique_integrity_on_flush() -> MagicMock:
    """Session whose .flush()/.commit() raises UNIQUE IntegrityError."""
    mock = MagicMock()
    mock.add.return_value = None
    mock.flush.side_effect = IntegrityError(
        "UNIQUE constraint failed: table.name", {}, None
    )
    mock.commit.side_effect = IntegrityError(
        "UNIQUE constraint failed: table.name", {}, None
    )
    mock.rollback.return_value = None
    query_mock = MagicMock()
    query_mock.filter_by.return_value.first.return_value = None
    query_mock.filter.return_value.first.return_value = None
    mock.query.return_value = query_mock
    return mock


def _mock_session_non_unique_integrity_on_flush() -> MagicMock:
    """Session whose .flush()/.commit() raises a non-UNIQUE IntegrityError."""
    mock = MagicMock()
    mock.add.return_value = None
    mock.flush.side_effect = IntegrityError(
        "CHECK constraint failed: some_check", {}, None
    )
    mock.commit.side_effect = IntegrityError(
        "CHECK constraint failed: some_check", {}, None
    )
    mock.rollback.return_value = None
    query_mock = MagicMock()
    query_mock.filter_by.return_value.first.return_value = None
    query_mock.filter.return_value.first.return_value = None
    mock.query.return_value = query_mock
    return mock


def _mock_session_integrity_on_flush() -> MagicMock:
    """Session whose .flush()/.commit() raises IntegrityError (generic)."""
    mock = MagicMock()
    mock.add.return_value = None
    mock.flush.side_effect = IntegrityError("FOREIGN KEY constraint failed", {}, None)
    mock.commit.side_effect = IntegrityError("FOREIGN KEY constraint failed", {}, None)
    mock.rollback.return_value = None
    query_mock = MagicMock()
    query_mock.filter_by.return_value.first.return_value = None
    query_mock.filter.return_value.first.return_value = None
    mock.query.return_value = query_mock
    return mock


def _capture_error(context: Context, fn, *args, **kwargs) -> None:
    """Call fn and capture any exception to context.repo_error."""
    try:
        fn(*args, **kwargs)
    except (
        DatabaseError,
        DuplicateToolError,
        ToolNotFoundError,
        DuplicateResourceError,
        DuplicateValidationAttachmentError,
    ) as exc:
        context.repo_error = exc
    except Exception as exc:
        # tenacity may wrap; unwrap to root cause
        cause = getattr(exc, "__cause__", None) or exc
        context.repo_error = cause


# ---------------------------------------------------------------------------
# Fake domain objects
# ---------------------------------------------------------------------------


def _make_fake_project() -> SimpleNamespace:
    """Minimal fake NamespacedProject for repository calls."""
    now = datetime.now(tz=UTC)
    proj = SimpleNamespace(
        name="test-project",
        namespace="local",
        namespaced_name="local/test-project",
        description="A test project",
        context_config=None,
        tags=[],
        created_at=now,
        updated_at=now,
    )
    return proj


def _make_fake_tool() -> SimpleNamespace:
    """Minimal fake Tool for repository calls."""
    return SimpleNamespace(
        name="local/test-tool",
        description="A test tool",
        tool_type=SimpleNamespace(value="tool"),
        source=SimpleNamespace(value="builtin"),
        input_schema=None,
        output_schema=None,
        capability=SimpleNamespace(
            read_only=False, writes=False, checkpointable=False, side_effects=[]
        ),
        config_yaml=None,
        resource_slots=[],
        tool_id=None,
    )


def _make_fake_automation_profile() -> SimpleNamespace:
    """Minimal fake AutomationProfile for repository calls."""
    return SimpleNamespace(
        name="local/test-profile",
        description="A test profile",
        schema_version="1.0",
        decompose_task=0.0,
        create_tool=0.0,
        select_tool=0.0,
        edit_code=0.0,
        execute_command=0.0,
        create_file=0.0,
        delete_content=0.0,
        access_network=0.0,
        install_dependency=0.0,
        modify_config=0.0,
        approve_plan=0.0,
        safety=SimpleNamespace(
            require_sandbox=True,
            require_checkpoints=True,
            allow_unsafe_tools=False,
        ),
    )


def _make_fake_resource_type() -> SimpleNamespace:
    """Minimal fake ResourceTypeSpec for repository calls."""
    return SimpleNamespace(
        name="test/resource-type",
        description="A test resource type",
        resource_kind=SimpleNamespace(value="physical"),
        sandbox_strategy=SimpleNamespace(value="copy_on_write"),
        user_addable=True,
        handler=None,
        cli_args=[],
        parent_types=[],
        child_types=[],
        auto_discovery=None,
        equivalence=None,
        capabilities=None,
        source=None,
    )


def _make_fake_resource() -> SimpleNamespace:
    """Minimal fake Resource for repository calls."""
    return SimpleNamespace(
        resource_id="01HGZ6FE0A0000000000000001",
        name="test/my-resource",
        resource_type_name="test/resource-type",
        classification="physical",
        description="A test resource",
        location="/tmp/test",
        capabilities=SimpleNamespace(writable=True),
        sandbox_strategy=None,
        content_hash=None,
        properties=None,
    )


def _make_fake_plan() -> SimpleNamespace:
    """Minimal fake Plan for LifecyclePlanRepository.

    Must have enough attributes for ``LifecyclePlanModel.from_domain()`` to
    succeed (it accesses many nested fields).
    """
    now = datetime.now(tz=UTC)
    return SimpleNamespace(
        identity=SimpleNamespace(
            plan_id="01HGZ6FE0A0000000000000099",
            parent_plan_id=None,
            root_plan_id=None,
            attempt=1,
        ),
        phase=SimpleNamespace(value="strategize"),
        processing_state=SimpleNamespace(value="queued"),
        namespaced_name=type(
            "_NS", (), {"namespace": "local", "__str__": lambda s: "local/test-plan"}
        )(),
        namespace="local",
        action_name="local/test-action",
        description="A test plan",
        definition_of_done="Done",
        strategy_actor="local/strategist",
        execution_actor="local/executor",
        review_actor=None,
        apply_actor=None,
        estimation_actor=None,
        invariant_actor=None,
        automation_profile=None,
        error_message=None,
        error_details=None,
        changeset_id=None,
        sandbox_refs=[],
        validation_summary=None,
        decision_root_id=None,
        execution_environment=None,
        execution_env_priority=None,
        reusable=False,
        read_only=False,
        created_by=None,
        tags=[],
        project_links=[],
        arguments={},
        arguments_order=[],
        invariants=[],
        effective_profile_snapshot="{}",
        timestamps=SimpleNamespace(
            created_at=now,
            updated_at=now,
            applied_at=None,
            strategize_started_at=None,
            strategize_completed_at=None,
            execute_started_at=None,
            execute_completed_at=None,
            apply_started_at=None,
        ),
    )


# ===========================================================================
# NamespacedProjectRepository steps
# ===========================================================================


@given(
    "a namespaced project repository with session raising UNIQUE IntegrityError on flush"
)
def step_nsproject_repo_unique_integrity(context: Context) -> None:
    mock = _mock_session_unique_integrity_on_flush()
    context.repo_under_test = NamespacedProjectRepository(session_factory=lambda: mock)
    context.repo_error = None


@given(
    "a namespaced project repository with session raising non-UNIQUE IntegrityError on flush"
)
def step_nsproject_repo_non_unique_integrity(context: Context) -> None:
    mock = _mock_session_non_unique_integrity_on_flush()
    context.repo_under_test = NamespacedProjectRepository(session_factory=lambda: mock)
    context.repo_error = None


@given("a namespaced project repository with session raising OperationalError on flush")
def step_nsproject_repo_op_error_flush(context: Context) -> None:
    mock = _mock_session_op_error_on_flush()
    context.repo_under_test = NamespacedProjectRepository(session_factory=lambda: mock)
    context.repo_error = None


@given("a namespaced project repository with session raising OperationalError on query")
def step_nsproject_repo_op_error_query(context: Context) -> None:
    mock = _mock_session_op_error_on_query()
    context.repo_under_test = NamespacedProjectRepository(session_factory=lambda: mock)
    context.repo_error = None


@when("a namespaced project is created and a repo error is expected")
def step_nsproject_create(context: Context) -> None:
    proj = _make_fake_project()
    _capture_error(context, context.repo_under_test.create, proj)


@when("a namespaced project is fetched by name and a repo error is expected")
def step_nsproject_get(context: Context) -> None:
    _capture_error(context, context.repo_under_test.get, "local/test-project")


@when("namespaced projects are listed and a repo error is expected")
def step_nsproject_list(context: Context) -> None:
    _capture_error(context, context.repo_under_test.list_projects)


@when("a namespaced project is updated and a repo error is expected")
def step_nsproject_update(context: Context) -> None:
    proj = _make_fake_project()
    _capture_error(context, context.repo_under_test.update, proj)


@when("a namespaced project is deleted and a repo error is expected")
def step_nsproject_delete(context: Context) -> None:
    _capture_error(context, context.repo_under_test.delete, "local/test-project")


# ===========================================================================
# ToolRepository steps
# ===========================================================================


@given("a tool repository with session raising UNIQUE IntegrityError on flush")
def step_tool_repo_unique_integrity(context: Context) -> None:
    mock = _mock_session_unique_integrity_on_flush()
    # .query().filter_by().first() returns None so the duplicate check passes
    context.repo_under_test = ToolRepository(session_factory=lambda: mock)
    context.repo_error = None


@given("a tool repository with session raising non-UNIQUE IntegrityError on flush")
def step_tool_repo_non_unique_integrity(context: Context) -> None:
    mock = _mock_session_non_unique_integrity_on_flush()
    context.repo_under_test = ToolRepository(session_factory=lambda: mock)
    context.repo_error = None


@given("a tool repository with session raising OperationalError on flush")
def step_tool_repo_op_error_flush(context: Context) -> None:
    mock = _mock_session_op_error_on_flush()
    context.repo_under_test = ToolRepository(session_factory=lambda: mock)
    context.repo_error = None


@given("a tool repository with session raising OperationalError on query")
def step_tool_repo_op_error_query(context: Context) -> None:
    mock = _mock_session_op_error_on_query()
    context.repo_under_test = ToolRepository(session_factory=lambda: mock)
    context.repo_error = None


@given("a tool repository backed by a clean in-memory database")
def step_tool_repo_clean_db(context: Context) -> None:
    engine = create_engine("sqlite:///:memory:")
    Base.metadata.create_all(engine)
    session_factory = sessionmaker(bind=engine)
    context.repo_under_test = ToolRepository(session_factory=session_factory)
    context.repo_error = None


@when("a tool is added and a repo error is expected")
def step_tool_add(context: Context) -> None:
    tool = _make_fake_tool()
    _capture_error(context, context.repo_under_test.add, tool)


@when("a tool is fetched by name and a repo error is expected")
def step_tool_get_by_name(context: Context) -> None:
    _capture_error(context, context.repo_under_test.get_by_name, "local/test-tool")


@when("all tools are listed and a repo error is expected")
def step_tool_list_all(context: Context) -> None:
    _capture_error(context, context.repo_under_test.list_all)


@when("a non-existent tool is updated and a repo error is expected")
def step_tool_update_nonexistent(context: Context) -> None:
    tool = _make_fake_tool()
    _capture_error(context, context.repo_under_test.update, tool)


@when("a non-existent tool is removed and a repo error is expected")
def step_tool_remove_nonexistent(context: Context) -> None:
    _capture_error(
        context, context.repo_under_test.remove, "01HGZ6FE0AXXXXXXXXXXXXXXXXX"
    )


# ===========================================================================
# AutomationProfileRepository steps
# ===========================================================================


@given(
    "an automation profile repository with session raising OperationalError on query"
)
def step_profile_repo_op_error_query(context: Context) -> None:
    mock = _mock_session_op_error_on_query()
    context.repo_under_test = AutomationProfileRepository(session_factory=lambda: mock)
    context.repo_error = None


@when("an automation profile is fetched by name and a repo error is expected")
def step_profile_get_by_name(context: Context) -> None:
    _capture_error(context, context.repo_under_test.get_by_name, "local/test-profile")


@when("all automation profiles are listed and a repo error is expected")
def step_profile_list_all(context: Context) -> None:
    _capture_error(context, context.repo_under_test.list_all)


@when("an automation profile is upserted and a repo error is expected")
def step_profile_upsert(context: Context) -> None:
    profile = _make_fake_automation_profile()
    _capture_error(context, context.repo_under_test.upsert, profile)


@when("an automation profile is deleted and a repo error is expected")
def step_profile_delete(context: Context) -> None:
    _capture_error(context, context.repo_under_test.delete, "local/test-profile")


# ===========================================================================
# ResourceTypeRepository steps
# ===========================================================================


@given("a resource type repository with session raising OperationalError on query")
def step_rt_repo_op_error_query(context: Context) -> None:
    mock = _mock_session_op_error_on_query()
    context.repo_under_test = ResourceTypeRepository(session_factory=lambda: mock)
    context.repo_error = None


@given("a resource type repository with session raising OperationalError on flush")
def step_rt_repo_op_error_flush(context: Context) -> None:
    mock = _mock_session_op_error_on_flush()
    context.repo_under_test = ResourceTypeRepository(session_factory=lambda: mock)
    context.repo_error = None


@given(
    "a resource type repository with session that passes query but raises non-UNIQUE IntegrityError on flush"
)
def step_rt_repo_non_unique_integrity(context: Context) -> None:
    mock = _mock_session_non_unique_integrity_on_flush()
    context.repo_under_test = ResourceTypeRepository(session_factory=lambda: mock)
    context.repo_error = None


@when("a resource type is created and a repo error is expected")
def step_rt_create(context: Context) -> None:
    rt = _make_fake_resource_type()
    _capture_error(context, context.repo_under_test.create, rt)


@when("a resource type is fetched by name and a repo error is expected")
def step_rt_get(context: Context) -> None:
    _capture_error(context, context.repo_under_test.get, "test/resource-type")


@when("resource types are listed and a repo error is expected")
def step_rt_list(context: Context) -> None:
    _capture_error(context, context.repo_under_test.list_types)


@when("a resource type is updated and a repo error is expected")
def step_rt_update(context: Context) -> None:
    rt = _make_fake_resource_type()
    _capture_error(context, context.repo_under_test.update, rt)


@when("a resource type is deleted and a repo error is expected")
def step_rt_delete(context: Context) -> None:
    _capture_error(context, context.repo_under_test.delete, "test/resource-type")


# ===========================================================================
# ResourceRepository steps
# ===========================================================================


def _make_resource_create_session_factory(integrity_msg: str):
    """Return a session_factory callable.

    Each call produces a fresh mock with its own counter so that retries
    get the same deterministic sequence: type-row on 1st query, None on
    2nd, then IntegrityError on flush.
    """

    def _factory() -> MagicMock:
        mock = MagicMock()
        mock.add.return_value = None
        mock.flush.side_effect = IntegrityError(integrity_msg, {}, None)
        mock.rollback.return_value = None

        type_row = MagicMock()
        type_row.name = "test/resource-type"
        call_count = {"n": 0}

        def _query_side_effect(*args):
            qm = MagicMock()

            def _filter_by_side(**kwargs):
                fm = MagicMock()
                call_count["n"] += 1
                if call_count["n"] == 1:
                    fm.first.return_value = type_row
                else:
                    fm.first.return_value = None
                return fm

            qm.filter_by = _filter_by_side
            return qm

        mock.query.side_effect = _query_side_effect
        return mock

    return _factory


@given("a resource repository with session raising OperationalError on query")
def step_resource_repo_op_error_query(context: Context) -> None:
    mock = _mock_session_op_error_on_query()
    context.repo_under_test = ResourceRepository(session_factory=lambda: mock)
    context.repo_error = None


@given("a resource repository with session raising OperationalError on flush")
def step_resource_repo_op_error_flush(context: Context) -> None:
    # ResourceRepository.create queries type first, so we need a factory
    # where queries return proper rows but flush raises OperationalError.
    def _factory() -> MagicMock:
        mock = MagicMock()
        mock.add.return_value = None
        mock.flush.side_effect = OperationalError("disk I/O error", {}, None)
        mock.rollback.return_value = None

        type_row = MagicMock()
        type_row.name = "test/resource-type"
        call_count = {"n": 0}

        def _query_side_effect(*args):
            qm = MagicMock()

            def _filter_by_side(**kwargs):
                fm = MagicMock()
                call_count["n"] += 1
                if call_count["n"] == 1:
                    fm.first.return_value = type_row
                else:
                    fm.first.return_value = None
                return fm

            qm.filter_by = _filter_by_side
            return qm

        mock.query.side_effect = _query_side_effect
        return mock

    context.repo_under_test = ResourceRepository(session_factory=_factory)
    context.repo_error = None


@given(
    "a resource repository with session that passes initial queries but raises UNIQUE IntegrityError on flush"
)
def step_resource_repo_unique_integrity(context: Context) -> None:
    factory = _make_resource_create_session_factory(
        "UNIQUE constraint failed: resources.namespaced_name"
    )
    context.repo_under_test = ResourceRepository(session_factory=factory)
    context.repo_error = None


@given(
    "a resource repository with session that passes initial queries but raises non-UNIQUE IntegrityError on flush"
)
def step_resource_repo_non_unique_integrity(context: Context) -> None:
    factory = _make_resource_create_session_factory(
        "CHECK constraint failed: some_check"
    )
    context.repo_under_test = ResourceRepository(session_factory=factory)
    context.repo_error = None


@when("a resource is created and a repo error is expected")
def step_resource_create(context: Context) -> None:
    res = _make_fake_resource()
    _capture_error(context, context.repo_under_test.create, res)


@when("a resource is fetched by ID and a repo error is expected")
def step_resource_get(context: Context) -> None:
    _capture_error(context, context.repo_under_test.get, "01HGZ6FE0A0000000000000001")


@when("a resource is fetched by name and a repo error is expected")
def step_resource_get_by_name(context: Context) -> None:
    _capture_error(context, context.repo_under_test.get_by_name, "test/my-resource")


@when("resources are listed and a repo error is expected")
def step_resource_list(context: Context) -> None:
    _capture_error(context, context.repo_under_test.list_resources)


@when("a resource is updated and a repo error is expected")
def step_resource_update(context: Context) -> None:
    res = _make_fake_resource()
    _capture_error(context, context.repo_under_test.update, res)


@when("a resource is deleted and a repo error is expected")
def step_resource_delete(context: Context) -> None:
    _capture_error(
        context, context.repo_under_test.delete, "01HGZ6FE0A0000000000000001"
    )


@when("a resource is resolved by name-or-id and a repo error is expected")
def step_resource_resolve(context: Context) -> None:
    _capture_error(
        context, context.repo_under_test.resolve_namespaced_name, "test/my-resource"
    )


# --- ResourceRepository DAG operations ---


def _make_dag_link_session_factory(flush_error):
    """Return a session_factory for DAG link operations.

    Each call produces a fresh mock with its own counter so retries
    get the same deterministic query sequence.

    ``flush_error`` should be an exception class or instance to raise
    on flush.
    """

    def _factory() -> MagicMock:
        mock = MagicMock()
        mock.rollback.return_value = None

        parent_row = MagicMock()
        parent_row.type_name = "test/parent-type"
        parent_row.resource_id = "PARENT000000000000000000001"

        child_row = MagicMock()
        child_row.type_name = "test/child-type"
        child_row.resource_id = "CHILD0000000000000000000001"

        type_row = MagicMock()
        type_row.allowed_child_types_json = None

        call_count = {"n": 0}

        def _query_side_effect(*args):
            qm = MagicMock()

            def _filter_by_side(**kwargs):
                fm = MagicMock()
                call_count["n"] += 1
                if call_count["n"] == 1:
                    fm.first.return_value = parent_row
                elif call_count["n"] == 2:
                    fm.first.return_value = child_row
                elif call_count["n"] == 3:
                    fm.first.return_value = type_row
                elif call_count["n"] == 4:
                    fm.first.return_value = None  # no existing link
                else:
                    fm.first.return_value = None
                    fm.all.return_value = []
                return fm

            qm.filter_by = _filter_by_side
            qm.filter.return_value = qm
            qm.all.return_value = []
            return qm

        mock.query.side_effect = _query_side_effect
        mock.add.return_value = None
        mock.flush.side_effect = flush_error
        return mock

    return _factory


@given(
    "a resource repository with session that has resources but raises IntegrityError on link flush"
)
def step_resource_repo_dag_integrity(context: Context) -> None:
    factory = _make_dag_link_session_factory(
        IntegrityError("FOREIGN KEY constraint failed", {}, None)
    )
    context.repo_under_test = ResourceRepository(session_factory=factory)
    context.repo_error = None


@given(
    "a resource repository with session that has resources but raises OperationalError on link flush"
)
def step_resource_repo_dag_op_error(context: Context) -> None:
    factory = _make_dag_link_session_factory(
        OperationalError("disk I/O error", {}, None)
    )
    context.repo_under_test = ResourceRepository(session_factory=factory)
    context.repo_error = None


@when("a resource child is linked and a repo error is expected")
def step_resource_link_child(context: Context) -> None:
    _capture_error(
        context,
        context.repo_under_test.link_child,
        "PARENT000000000000000000001",
        "CHILD0000000000000000000001",
    )


@when("a resource child is unlinked and a repo error is expected")
def step_resource_unlink_child(context: Context) -> None:
    _capture_error(
        context,
        context.repo_under_test.unlink_child,
        "PARENT000000000000000000001",
        "CHILD0000000000000000000001",
    )


@when("resource children are fetched and a repo error is expected")
def step_resource_get_children(context: Context) -> None:
    _capture_error(
        context,
        context.repo_under_test.get_children,
        "PARENT000000000000000000001",
    )


@when("resource parents are fetched and a repo error is expected")
def step_resource_get_parents(context: Context) -> None:
    _capture_error(
        context,
        context.repo_under_test.get_parents,
        "CHILD0000000000000000000001",
    )


# ===========================================================================
# ProjectResourceLinkRepository steps
# ===========================================================================


@given(
    "a project resource link repository with session that passes query but raises IntegrityError on flush"
)
def step_link_repo_integrity(context: Context) -> None:
    mock = _mock_session_integrity_on_flush()
    context.repo_under_test = ProjectResourceLinkRepository(
        session_factory=lambda: mock
    )
    context.repo_error = None


@given(
    "a project resource link repository with session raising OperationalError on flush"
)
def step_link_repo_op_error_flush(context: Context) -> None:
    mock = _mock_session_op_error_on_flush()
    context.repo_under_test = ProjectResourceLinkRepository(
        session_factory=lambda: mock
    )
    context.repo_error = None


@given(
    "a project resource link repository with session raising OperationalError on query"
)
def step_link_repo_op_error_query(context: Context) -> None:
    mock = _mock_session_op_error_on_query()
    context.repo_under_test = ProjectResourceLinkRepository(
        session_factory=lambda: mock
    )
    context.repo_error = None


@when("a project resource link is created and a repo error is expected")
def step_link_create(context: Context) -> None:
    _capture_error(
        context,
        context.repo_under_test.create_link,
        "local/test-project",
        "01HGZ6FE0A0000000000000001",
    )


@when("project resource links are listed and a repo error is expected")
def step_link_list(context: Context) -> None:
    _capture_error(
        context,
        context.repo_under_test.list_links,
        "local/test-project",
    )


@when("a project resource link is fetched and a repo error is expected")
def step_link_get(context: Context) -> None:
    _capture_error(
        context,
        context.repo_under_test.get_link,
        "01HGZ6FE0A0000000000000001",
    )


@when("a project resource link is removed and a repo error is expected")
def step_link_remove(context: Context) -> None:
    _capture_error(
        context,
        context.repo_under_test.remove_link,
        "01HGZ6FE0A0000000000000001",
    )


# ===========================================================================
# ValidationAttachmentRepository steps
# ===========================================================================


@given(
    "a validation attachment repository with session that passes query but raises UNIQUE IntegrityError on flush"
)
def step_val_repo_unique_integrity(context: Context) -> None:
    mock = _mock_session_unique_integrity_on_flush()
    context.repo_under_test = ValidationAttachmentRepository(
        session_factory=lambda: mock
    )
    context.repo_error = None


@given(
    "a validation attachment repository with session that passes query but raises non-UNIQUE IntegrityError on flush"
)
def step_val_repo_non_unique_integrity(context: Context) -> None:
    mock = _mock_session_non_unique_integrity_on_flush()
    context.repo_under_test = ValidationAttachmentRepository(
        session_factory=lambda: mock
    )
    context.repo_error = None


@given(
    "a validation attachment repository with session raising OperationalError on flush"
)
def step_val_repo_op_error_flush(context: Context) -> None:
    mock = _mock_session_op_error_on_flush()
    context.repo_under_test = ValidationAttachmentRepository(
        session_factory=lambda: mock
    )
    context.repo_error = None


@given(
    "a validation attachment repository with session raising OperationalError on query"
)
def step_val_repo_op_error_query(context: Context) -> None:
    mock = _mock_session_op_error_on_query()
    context.repo_under_test = ValidationAttachmentRepository(
        session_factory=lambda: mock
    )
    context.repo_error = None


@when("a validation is attached and a repo error is expected")
def step_val_attach(context: Context) -> None:
    _capture_error(
        context,
        context.repo_under_test.attach,
        "local/test-validation",
        "01HGZ6FE0A0000000000000001",
        "required",
    )


@when("a validation is detached and a repo error is expected")
def step_val_detach(context: Context) -> None:
    _capture_error(
        context,
        context.repo_under_test.detach,
        "01HGZ6FE0A0000000000000001",
    )


@when("validation attachments are listed and a repo error is expected")
def step_val_list(context: Context) -> None:
    _capture_error(
        context,
        context.repo_under_test.list_for_resource,
        "01HGZ6FE0A0000000000000001",
    )


# ===========================================================================
# LifecyclePlanRepository steps
# ===========================================================================


@given(
    "a lifecycle plan repository with session raising non-UNIQUE IntegrityError on flush"
)
def step_plan_repo_non_unique_integrity(context: Context) -> None:
    mock = _mock_session_non_unique_integrity_on_flush()
    context.repo_under_test = LifecyclePlanRepository(session_factory=lambda: mock)
    context.repo_error = None


@given("a lifecycle plan repository with session raising OperationalError on flush")
def step_plan_repo_op_error_flush(context: Context) -> None:
    mock = _mock_session_op_error_on_flush()
    context.repo_under_test = LifecyclePlanRepository(session_factory=lambda: mock)
    context.repo_error = None


@given("a lifecycle plan repository with session raising OperationalError on query")
def step_plan_repo_op_error_query(context: Context) -> None:
    mock = _mock_session_op_error_on_query()
    context.repo_under_test = LifecyclePlanRepository(session_factory=lambda: mock)
    context.repo_error = None


@when("a lifecycle plan is created and a repo error is expected")
def step_plan_create(context: Context) -> None:
    plan = _make_fake_plan()
    _capture_error(context, context.repo_under_test.create, plan)


@when("a lifecycle plan is fetched by ID and a repo error is expected")
def step_plan_get(context: Context) -> None:
    _capture_error(context, context.repo_under_test.get, "01HGZ6FE0A0000000000000099")


@when("a lifecycle plan is fetched by name and a repo error is expected")
def step_plan_get_by_name(context: Context) -> None:
    _capture_error(context, context.repo_under_test.get_by_name, "local/test-plan")


@when("a lifecycle plan is updated and a repo error is expected")
def step_plan_update(context: Context) -> None:
    plan = _make_fake_plan()
    _capture_error(context, context.repo_under_test.update, plan)


@when("a lifecycle plan is deleted and a repo error is expected")
def step_plan_delete(context: Context) -> None:
    _capture_error(
        context, context.repo_under_test.delete, "01HGZ6FE0A0000000000000099"
    )


@when("lifecycle plans are listed and a repo error is expected")
def step_plan_list(context: Context) -> None:
    _capture_error(context, context.repo_under_test.list_plans)


@when("lifecycle plans are counted and a repo error is expected")
def step_plan_count(context: Context) -> None:
    _capture_error(context, context.repo_under_test.count)


# ===========================================================================
# Common assertion steps (unique to this file)
# ===========================================================================


@then('a repo DatabaseError should be raised containing "{fragment}"')
def step_check_repo_db_error(context: Context, fragment: str) -> None:
    assert context.repo_error is not None, (
        "Expected a DatabaseError but no error was raised"
    )
    assert isinstance(context.repo_error, DatabaseError), (
        f"Expected DatabaseError, got {type(context.repo_error).__name__}: "
        f"{context.repo_error}"
    )
    assert fragment in str(context.repo_error), (
        f"Expected '{fragment}' in error message, got: {context.repo_error}"
    )


@then("a repo DuplicateToolError should be raised")
def step_check_duplicate_tool_error(context: Context) -> None:
    assert context.repo_error is not None, (
        "Expected a DuplicateToolError but no error was raised"
    )
    assert isinstance(context.repo_error, DuplicateToolError), (
        f"Expected DuplicateToolError, got {type(context.repo_error).__name__}: "
        f"{context.repo_error}"
    )


@then("a repo ToolNotFoundError should be raised")
def step_check_tool_not_found_error(context: Context) -> None:
    assert context.repo_error is not None, (
        "Expected a ToolNotFoundError but no error was raised"
    )
    assert isinstance(context.repo_error, ToolNotFoundError), (
        f"Expected ToolNotFoundError, got {type(context.repo_error).__name__}: "
        f"{context.repo_error}"
    )


@then("a repo DuplicateResourceError should be raised")
def step_check_duplicate_resource_error(context: Context) -> None:
    assert context.repo_error is not None, (
        "Expected a DuplicateResourceError but no error was raised"
    )
    assert isinstance(context.repo_error, DuplicateResourceError), (
        f"Expected DuplicateResourceError, got {type(context.repo_error).__name__}: "
        f"{context.repo_error}"
    )


@then("a repo DuplicateValidationAttachmentError should be raised")
def step_check_duplicate_val_attachment_error(context: Context) -> None:
    assert context.repo_error is not None, (
        "Expected a DuplicateValidationAttachmentError but no error was raised"
    )
    assert isinstance(context.repo_error, DuplicateValidationAttachmentError), (
        f"Expected DuplicateValidationAttachmentError, "
        f"got {type(context.repo_error).__name__}: {context.repo_error}"
    )
