"""Step definitions for repository error resilience and edge-case coverage.

Covers uncovered error-handling branches (OperationalError / IntegrityError)
and conditional logic paths across SessionRepository, SessionMessageRepository,
ToolRegistryRepository, ValidationAttachmentRepository, LifecyclePlanRepository,
ResourceRepository, and ResourceTypeRepository.

Strategy: broken-session mocks force every ``except`` handler to fire.
The ``@database_retry`` decorator retries 3 times then re-raises ``DatabaseError``.
"""

from __future__ import annotations

import json as _json
from datetime import UTC, datetime
from unittest.mock import MagicMock

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

from cleveragents.core.exceptions import DatabaseError
from cleveragents.infrastructure.database.models import Base
from cleveragents.infrastructure.database.repositories import (
    LifecyclePlanRepository,
    ResourceRepository,
    ResourceTypeRepository,
    SessionMessageRepository,
    SessionRepository,
    ToolRegistryRepository,
    ValidationAttachmentRepository,
)

# ── Helpers ────────────────────────────────────────────────────────────────

_CB32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
_CTR = 9000


def _ulid() -> str:
    global _CTR
    _CTR += 1
    n = _CTR
    s = ""
    for _ in range(16):
        s = _CB32[n % 32] + s
        n //= 32
    return f"01HGZ6FE0A{s}"


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


# ── Broken session factories ──────────────────────────────────────────────


def _broken_on_query() -> MagicMock:
    m = MagicMock()
    m.query.side_effect = OperationalError("connection lost", {}, None)
    m.rollback.return_value = None
    return m


def _broken_on_flush() -> MagicMock:
    m = MagicMock()
    m.add.return_value = None
    m.flush.side_effect = OperationalError("disk I/O error", {}, None)
    m.rollback.return_value = None
    return m


def _integrity_on_flush() -> MagicMock:
    m = MagicMock()
    m.add.return_value = None
    m.flush.side_effect = IntegrityError("CHECK constraint failed", {}, None)
    m.rollback.return_value = None
    return m


def _broken_on_flush_for_resource() -> MagicMock:
    """Mock that passes query checks but explodes on flush.

    ``ResourceRepository.create`` does ``session.query(...).filter_by(...).first()``
    twice: once for the type check (must return truthy) and once for the
    duplicate-name check (must return ``None``).  We use ``side_effect`` to
    return the right thing at the right time, then blow up on flush.
    """
    m = MagicMock()

    # Each .query().filter_by().first() chain returns a new MagicMock by
    # default (truthy).  We need the *second* .first() call to return None.
    call_count = {"n": 0}
    _inner_filter = MagicMock()

    def _first():
        call_count["n"] += 1
        if call_count["n"] == 1:
            return MagicMock()  # type exists
        return None  # no duplicate name

    _inner_filter.first = _first
    m.query.return_value.filter_by.return_value = _inner_filter
    m.add.return_value = None
    m.flush.side_effect = OperationalError("disk I/O error", {}, None)
    m.rollback.return_value = None
    return m


def _integrity_on_flush_for_resource() -> MagicMock:
    """Like ``_broken_on_flush_for_resource`` but raises non-unique IntegrityError."""
    m = MagicMock()
    call_count = {"n": 0}
    _inner_filter = MagicMock()

    def _first():
        call_count["n"] += 1
        if call_count["n"] == 1:
            return MagicMock()
        return None

    _inner_filter.first = _first
    m.query.return_value.filter_by.return_value = _inner_filter
    m.add.return_value = None
    m.flush.side_effect = IntegrityError("CHECK constraint failed", {}, None)
    m.rollback.return_value = None
    return m


def _broken_on_flush_for_rt() -> MagicMock:
    """Mock for ResourceTypeRepository.create: passes duplicate check, fails on flush.

    ``ResourceTypeRepository.create`` does one
    ``session.query(...).filter_by(...).first()`` call to check for duplicates.
    That must return ``None`` so the create proceeds to flush, which explodes.
    """
    m = MagicMock()
    m.query.return_value.filter_by.return_value.first.return_value = None
    m.add.return_value = None
    m.flush.side_effect = OperationalError("disk I/O error", {}, None)
    m.rollback.return_value = None
    return m


def _integrity_on_flush_for_rt() -> MagicMock:
    """Like ``_broken_on_flush_for_rt`` but raises non-unique IntegrityError."""
    m = MagicMock()
    m.query.return_value.filter_by.return_value.first.return_value = None
    m.add.return_value = None
    m.flush.side_effect = IntegrityError("CHECK constraint failed", {}, None)
    m.rollback.return_value = None
    return m


def _run_expecting_db_error(context: Context, fn):
    """Call *fn* and capture DatabaseError (or its retry wrapper)."""
    context.cov_error = None
    try:
        fn()
    except DatabaseError as exc:
        context.cov_error = exc
    except Exception as exc:
        cause = getattr(exc, "__cause__", None) or exc
        context.cov_error = cause


def _setup_db() -> tuple[sessionmaker, Session]:
    engine = create_engine("sqlite:///:memory:")

    @event.listens_for(engine, "connect")
    def _fk(dbapi_conn, _rec):
        dbapi_conn.cursor().execute("PRAGMA foreign_keys=ON")

    Base.metadata.create_all(engine)
    sf = sessionmaker(bind=engine, expire_on_commit=False)
    session = sf()
    return sf, session


# ══════════════════════════════════════════════════════════════════════════
# Background / Given steps
# ══════════════════════════════════════════════════════════════════════════


@given("a clean in-memory database for repository coverage")
def step_clean_db(context: Context) -> None:
    sf, session = _setup_db()
    context.cov_sf = sf
    context.cov_session = session
    context.cov_error = None


@given("a session repository backed by the coverage database")
def step_session_repo(context: Context) -> None:
    context.cov_session_repo = SessionRepository(
        session_factory=lambda: context.cov_session
    )


@given("a tool registry repository backed by the coverage database")
def step_tool_repo(context: Context) -> None:
    context.cov_tool_repo = ToolRegistryRepository(
        session_factory=lambda: context.cov_session
    )


@given("a validation attachment repository backed by the coverage database")
def step_val_repo(context: Context) -> None:
    context.cov_val_repo = ValidationAttachmentRepository(
        session_factory=lambda: context.cov_session
    )


@given("a resource type and resource repository backed by the coverage database")
def step_rt_and_res_repo(context: Context) -> None:
    context.cov_rt_repo = ResourceTypeRepository(
        session_factory=lambda: context.cov_session
    )
    context.cov_res_repo = ResourceRepository(
        session_factory=lambda: context.cov_session
    )


@given('a registered validation tool named "{name}"')
def step_register_validation_tool(context: Context, name: str) -> None:
    from cleveragents.infrastructure.database.models import ToolModel

    now = _now_iso()
    model = ToolModel(
        name=name,
        namespace=name.split("/")[0],
        short_name=name.split("/")[1],
        description="Test validation",
        tool_type="validation",
        source="builtin",
        timeout=300,
        mode="required",
        created_at=now,
        updated_at=now,
    )
    context.cov_session.add(model)
    context.cov_session.commit()


@given('a seeded resource type "{name}"')
def step_seed_resource_type(context: Context, name: str) -> None:
    from cleveragents.domain.models.core.resource_type import (
        ResourceKind,
        ResourceTypeSpec,
        SandboxStrategy,
    )

    spec = ResourceTypeSpec(
        name=name,
        description="Coverage seed type",
        resource_kind=ResourceKind.PHYSICAL,
        sandbox_strategy=SandboxStrategy.NONE,
        user_addable=True,
        cli_args=[],
        parent_types=[],
        child_types=[],
        auto_discovery=None,
        equivalence=None,
        handler=None,
        built_in=False,
    )
    context.cov_rt_repo.create(spec)
    context.cov_session.commit()


@given('a resource "{name}" of type "{type_name}"')
def step_create_resource(context: Context, name: str, type_name: str) -> None:
    from cleveragents.domain.models.core.resource import (
        PhysVirt,
        Resource,
        ResourceCapabilities,
    )

    now = datetime.now(tz=UTC)
    res = Resource(
        resource_id=_ulid(),
        name=name,
        resource_type_name=type_name,
        classification=PhysVirt.PHYSICAL,
        description="Coverage resource",
        properties={},
        location="/tmp/cov",
        content_hash=None,
        sandbox_strategy=None,
        capabilities=ResourceCapabilities(),
        created_at=now,
        updated_at=now,
    )
    context.cov_res_repo.create(res)
    context.cov_session.commit()


# ── Broken-session Given steps ────────────────────────────────────────────


@given("a session repository whose session raises OperationalError on flush")
def step_session_repo_broken_flush(context: Context) -> None:
    context.cov_error = None
    context.cov_session_repo = SessionRepository(
        session_factory=lambda: _broken_on_flush()
    )


@given("a session repository whose session raises a non-unique IntegrityError on flush")
def step_session_repo_integrity_flush(context: Context) -> None:
    context.cov_error = None
    context.cov_session_repo = SessionRepository(
        session_factory=lambda: _integrity_on_flush()
    )


@given("a session repository whose session raises OperationalError on query")
def step_session_repo_broken_query(context: Context) -> None:
    context.cov_error = None
    context.cov_session_repo = SessionRepository(
        session_factory=lambda: _broken_on_query()
    )


@given("a session message repository whose session raises OperationalError on flush")
def step_msg_repo_broken_flush(context: Context) -> None:
    context.cov_error = None
    context.cov_msg_repo = SessionMessageRepository(
        session_factory=lambda: _broken_on_flush()
    )


@given("a session message repository whose session raises OperationalError on query")
def step_msg_repo_broken_query(context: Context) -> None:
    context.cov_error = None
    context.cov_msg_repo = SessionMessageRepository(
        session_factory=lambda: _broken_on_query()
    )


@given("a tool registry repository whose session raises OperationalError on flush")
def step_tool_repo_broken_flush(context: Context) -> None:
    context.cov_error = None
    context.cov_tool_repo = ToolRegistryRepository(
        session_factory=lambda: _broken_on_flush()
    )


@given(
    "a tool registry repository whose session raises a non-unique IntegrityError on flush"
)
def step_tool_repo_integrity_flush(context: Context) -> None:
    context.cov_error = None
    context.cov_tool_repo = ToolRegistryRepository(
        session_factory=lambda: _integrity_on_flush()
    )


@given("a tool registry repository whose session raises OperationalError on query")
def step_tool_repo_broken_query(context: Context) -> None:
    context.cov_error = None
    context.cov_tool_repo = ToolRegistryRepository(
        session_factory=lambda: _broken_on_query()
    )


@given(
    "a validation attachment repository whose session raises OperationalError on flush"
)
def step_val_repo_broken_flush(context: Context) -> None:
    context.cov_error = None
    context.cov_val_repo = ValidationAttachmentRepository(
        session_factory=lambda: _broken_on_flush()
    )


@given(
    "a validation attachment repository whose session raises OperationalError on query"
)
def step_val_repo_broken_query(context: Context) -> None:
    context.cov_error = None
    context.cov_val_repo = ValidationAttachmentRepository(
        session_factory=lambda: _broken_on_query()
    )


@given(
    "a lifecycle plan repository whose session raises a non-unique IntegrityError on flush"
)
def step_lp_repo_integrity_flush(context: Context) -> None:
    context.cov_error = None
    context.cov_lp_repo = LifecyclePlanRepository(
        session_factory=lambda: _integrity_on_flush()
    )


@given("a lifecycle plan repository whose session raises OperationalError on flush")
def step_lp_repo_broken_flush(context: Context) -> None:
    context.cov_error = None
    context.cov_lp_repo = LifecyclePlanRepository(
        session_factory=lambda: _broken_on_flush()
    )


@given("a lifecycle plan repository whose session raises OperationalError on query")
def step_lp_repo_broken_query(context: Context) -> None:
    context.cov_error = None
    context.cov_lp_repo = LifecyclePlanRepository(
        session_factory=lambda: _broken_on_query()
    )


@given("a resource repository whose session raises OperationalError on flush")
def step_res_repo_broken_flush(context: Context) -> None:
    context.cov_error = None
    context.cov_res_repo = ResourceRepository(
        session_factory=lambda: _broken_on_flush_for_resource()
    )


@given(
    "a resource repository whose session raises a non-unique IntegrityError on flush"
)
def step_res_repo_integrity_flush(context: Context) -> None:
    context.cov_error = None
    context.cov_res_repo = ResourceRepository(
        session_factory=lambda: _integrity_on_flush_for_resource()
    )


@given("a resource repository whose session raises OperationalError on query")
def step_res_repo_broken_query(context: Context) -> None:
    context.cov_error = None
    context.cov_res_repo = ResourceRepository(
        session_factory=lambda: _broken_on_query()
    )


@given("a resource type repository whose session raises OperationalError on flush")
def step_rt_repo_broken_flush(context: Context) -> None:
    context.cov_error = None
    context.cov_rt_repo = ResourceTypeRepository(
        session_factory=lambda: _broken_on_flush_for_rt()
    )


@given(
    "a resource type repository whose session raises a non-unique IntegrityError on flush"
)
def step_rt_repo_integrity_flush(context: Context) -> None:
    context.cov_error = None
    context.cov_rt_repo = ResourceTypeRepository(
        session_factory=lambda: _integrity_on_flush_for_rt()
    )


@given("a resource type repository whose session raises OperationalError on query")
def step_rt_repo_broken_query(context: Context) -> None:
    context.cov_error = None
    context.cov_rt_repo = ResourceTypeRepository(
        session_factory=lambda: _broken_on_query()
    )


# ══════════════════════════════════════════════════════════════════════════
# When steps — Session Repository
# ══════════════════════════════════════════════════════════════════════════


@when('two sessions are created with actor "{actor}"')
def step_create_sessions_with_actor(context: Context, actor: str) -> None:
    from cleveragents.infrastructure.database.models import SessionModel

    for _ in range(2):
        m = SessionModel(
            session_id=_ulid(),
            actor_name=actor,
            namespace="local",
            created_at=_now_iso(),
            updated_at=_now_iso(),
        )
        context.cov_session.add(m)
    context.cov_session.commit()


@when('one session is created with actor "{actor}"')
def step_create_one_session_with_actor(context: Context, actor: str) -> None:
    from cleveragents.infrastructure.database.models import SessionModel

    m = SessionModel(
        session_id=_ulid(),
        actor_name=actor,
        namespace="local",
        created_at=_now_iso(),
        updated_at=_now_iso(),
    )
    context.cov_session.add(m)
    context.cov_session.commit()


@when('sessions are listed filtered by actor "{actor}"')
def step_list_sessions_by_actor(context: Context, actor: str) -> None:
    context.cov_filtered_sessions = context.cov_session_repo.list_all(actor_name=actor)


@when("a session update is attempted for a non-existent session identifier")
def step_update_nonexistent_session(context: Context) -> None:
    from cleveragents.domain.models.core.session import Session, SessionTokenUsage

    fake = Session(
        session_id="01ZZZZZZZZZZZZZZZZZZZZZZZZ",
        actor_name=None,
        namespace="local",
        linked_plan_ids=[],
        token_usage=SessionTokenUsage(),
        metadata={},
        messages=[],
        created_at=datetime.now(tz=UTC),
        updated_at=datetime.now(tz=UTC),
    )
    _run_expecting_db_error(context, lambda: context.cov_session_repo.update(fake))


@when("a session is created and a database error is expected")
def step_session_create_error(context: Context) -> None:
    from cleveragents.domain.models.core.session import Session as DomainSession

    domain_obj = DomainSession(
        session_id=_ulid(),
        namespace="local",
    )
    _run_expecting_db_error(
        context, lambda: context.cov_session_repo.create(domain_obj)
    )


@when("a session is retrieved by identifier and a database error is expected")
def step_session_get_error(context: Context) -> None:
    _run_expecting_db_error(
        context, lambda: context.cov_session_repo.get_by_id("01FAKEID")
    )


@when("sessions are listed and a database error is expected")
def step_session_list_error(context: Context) -> None:
    _run_expecting_db_error(context, lambda: context.cov_session_repo.list_all())


@when("a session is deleted and a database error is expected")
def step_session_delete_error(context: Context) -> None:
    _run_expecting_db_error(
        context, lambda: context.cov_session_repo.delete("01FAKEID")
    )


@when("a session is updated and a database error is expected")
def step_session_update_error(context: Context) -> None:
    from cleveragents.domain.models.core.session import Session as DomainSession

    fake = DomainSession(
        session_id=_ulid(),
        namespace="local",
    )
    _run_expecting_db_error(context, lambda: context.cov_session_repo.update(fake))


# ══════════════════════════════════════════════════════════════════════════
# When steps — Session Message Repository
# ══════════════════════════════════════════════════════════════════════════


@when("a message is appended and a database error is expected")
def step_msg_append_error(context: Context) -> None:
    from cleveragents.domain.models.core.session import MessageRole, SessionMessage

    msg = SessionMessage(
        message_id=_ulid(),
        role=MessageRole.USER,
        content="test",
        sequence=0,
        timestamp=datetime.now(tz=UTC),
    )
    _run_expecting_db_error(context, lambda: context.cov_msg_repo.append(_ulid(), msg))


@when("messages are retrieved and a database error is expected")
def step_msg_get_error(context: Context) -> None:
    _run_expecting_db_error(
        context, lambda: context.cov_msg_repo.get_for_session("01FAKE")
    )


@when("messages are counted and a database error is expected")
def step_msg_count_error(context: Context) -> None:
    _run_expecting_db_error(
        context, lambda: context.cov_msg_repo.count_for_session("01FAKE")
    )


# ══════════════════════════════════════════════════════════════════════════
# When steps — Tool Registry Repository
# ══════════════════════════════════════════════════════════════════════════


@when('a tool update is attempted for non-existent tool "{name}"')
def step_update_nonexistent_tool(context: Context, name: str) -> None:
    tool_dict = {
        "name": name,
        "description": "phantom",
        "tool_type": "tool",
        "source": "builtin",
        "timeout": 300,
    }
    _run_expecting_db_error(context, lambda: context.cov_tool_repo.update(tool_dict))


@when("a tool is created and a database error is expected")
def step_tool_create_error(context: Context) -> None:
    tool_dict = {
        "name": "local/broken-tool",
        "description": "broken",
        "tool_type": "tool",
        "source": "builtin",
        "timeout": 300,
    }
    _run_expecting_db_error(context, lambda: context.cov_tool_repo.create(tool_dict))


@when("a tool is retrieved by name and a database error is expected")
def step_tool_get_error(context: Context) -> None:
    _run_expecting_db_error(
        context, lambda: context.cov_tool_repo.get_by_name("local/broken")
    )


@when("tools are listed and a database error is expected")
def step_tool_list_error(context: Context) -> None:
    _run_expecting_db_error(context, lambda: context.cov_tool_repo.list_all())


@when("a tool is updated and a database error is expected")
def step_tool_update_error(context: Context) -> None:
    _run_expecting_db_error(
        context, lambda: context.cov_tool_repo.update({"name": "local/broken"})
    )


@when("a tool is deleted and a database error is expected")
def step_tool_delete_error(context: Context) -> None:
    _run_expecting_db_error(
        context, lambda: context.cov_tool_repo.delete("local/broken")
    )


# ══════════════════════════════════════════════════════════════════════════
# When steps — Validation Attachment Repository
# ══════════════════════════════════════════════════════════════════════════


@when('a validation is attached to resource "{res}" with arguments')
def step_attach_with_args(context: Context, res: str) -> None:
    context.cov_attachment = context.cov_val_repo.attach(
        validation_name="local/schema-check",
        resource_id=res,
        mode="required",
        args={"severity": "error", "max_depth": 5},
    )
    context.cov_session.commit()


@when("an attachment is retrieved by non-existent identifier")
def step_get_nonexistent_attachment(context: Context) -> None:
    context.cov_attachment_result = context.cov_val_repo.get_by_id(
        "01ZZZZZZZZZZZZZZZZZZZZZZZZ"
    )


@when("a validation is attached and a database error is expected")
def step_val_attach_error(context: Context) -> None:
    _run_expecting_db_error(
        context,
        lambda: context.cov_val_repo.attach(
            validation_name="local/x", resource_id="r", mode="required"
        ),
    )


@when("a validation is detached and a database error is expected")
def step_val_detach_error(context: Context) -> None:
    _run_expecting_db_error(context, lambda: context.cov_val_repo.detach("01FAKE"))


@when("attachments are listed and a database error is expected")
def step_val_list_error(context: Context) -> None:
    _run_expecting_db_error(
        context, lambda: context.cov_val_repo.list_for_resource("r")
    )


@when("an attachment is retrieved and a database error is expected")
def step_val_get_error(context: Context) -> None:
    _run_expecting_db_error(context, lambda: context.cov_val_repo.get_by_id("01FAKE"))


# ══════════════════════════════════════════════════════════════════════════
# When steps — Lifecycle Plan Repository
# ══════════════════════════════════════════════════════════════════════════


def _make_lifecycle_plan():
    from cleveragents.domain.models.core.plan import (
        NamespacedName,
        PlanIdentity,
        PlanPhase,
        PlanTimestamps,
        ProcessingState,
    )
    from cleveragents.domain.models.core.plan import Plan as V3Plan

    now = datetime.now()
    return V3Plan(
        identity=PlanIdentity(plan_id=_ulid(), attempt=1),
        namespaced_name=NamespacedName(namespace="local", name=f"cov-plan-{_ulid()}"),
        action_name="local/test-action",
        description="Coverage plan",
        definition_of_done="Done",
        strategy_actor="local/s",
        execution_actor="local/e",
        phase=PlanPhase("action"),
        processing_state=ProcessingState("queued"),
        timestamps=PlanTimestamps(created_at=now, updated_at=now),
        project_links=[],
        arguments={},
        arguments_order=[],
        invariants=[],
        reusable=True,
        read_only=False,
        created_by=None,
        tags=[],
    )


@when("a lifecycle plan is created and a database error is expected")
def step_lp_create_error(context: Context) -> None:
    plan = _make_lifecycle_plan()
    _run_expecting_db_error(context, lambda: context.cov_lp_repo.create(plan))


@when("a lifecycle plan is retrieved and a database error is expected")
def step_lp_get_error(context: Context) -> None:
    _run_expecting_db_error(context, lambda: context.cov_lp_repo.get(_ulid()))


@when("a lifecycle plan is retrieved by name and a database error is expected")
def step_lp_get_by_name_error(context: Context) -> None:
    _run_expecting_db_error(
        context, lambda: context.cov_lp_repo.get_by_name("local/phantom")
    )


@when("a lifecycle plan is updated and a database error is expected")
def step_lp_update_error(context: Context) -> None:
    plan = _make_lifecycle_plan()
    _run_expecting_db_error(context, lambda: context.cov_lp_repo.update(plan))


@when("a lifecycle plan is deleted and a database error is expected")
def step_lp_delete_error(context: Context) -> None:
    _run_expecting_db_error(context, lambda: context.cov_lp_repo.delete(_ulid()))


@when("lifecycle plans are listed and a database error is expected")
def step_lp_list_error(context: Context) -> None:
    _run_expecting_db_error(context, lambda: context.cov_lp_repo.list_plans())


@when("lifecycle plans are counted and a database error is expected")
def step_lp_count_error(context: Context) -> None:
    _run_expecting_db_error(context, lambda: context.cov_lp_repo.count())


# ══════════════════════════════════════════════════════════════════════════
# When steps — Resource Repository
# ══════════════════════════════════════════════════════════════════════════


@when('resources are listed filtered by namespace "{ns}"')
def step_list_resources_by_ns(context: Context, ns: str) -> None:
    context.cov_filtered_resources = context.cov_res_repo.list_resources(namespace=ns)


@when("a resource is created and a database error is expected")
def step_res_create_error(context: Context) -> None:
    from cleveragents.domain.models.core.resource import (
        PhysVirt,
        Resource,
        ResourceCapabilities,
    )

    now = datetime.now(tz=UTC)
    res = Resource(
        resource_id=_ulid(),
        name="broken/res",
        resource_type_name="broken/type",
        classification=PhysVirt.PHYSICAL,
        description="broken",
        properties={},
        location="/tmp",
        content_hash=None,
        sandbox_strategy=None,
        capabilities=ResourceCapabilities(),
        created_at=now,
        updated_at=now,
    )
    _run_expecting_db_error(context, lambda: context.cov_res_repo.create(res))


@when("a resource is retrieved and a database error is expected")
def step_res_get_error(context: Context) -> None:
    _run_expecting_db_error(context, lambda: context.cov_res_repo.get(_ulid()))


@when("a resource is retrieved by name and a database error is expected")
def step_res_get_by_name_error(context: Context) -> None:
    _run_expecting_db_error(
        context, lambda: context.cov_res_repo.get_by_name("broken/res")
    )


@when("resources are listed and a database error is expected")
def step_res_list_error(context: Context) -> None:
    _run_expecting_db_error(context, lambda: context.cov_res_repo.list_resources())


@when("a resource is updated and a database error is expected")
def step_res_update_error(context: Context) -> None:
    from cleveragents.domain.models.core.resource import (
        PhysVirt,
        Resource,
        ResourceCapabilities,
    )

    now = datetime.now(tz=UTC)
    res = Resource(
        resource_id=_ulid(),
        name="broken/res",
        resource_type_name="broken/type",
        classification=PhysVirt.PHYSICAL,
        description="broken",
        properties={},
        location="/tmp",
        content_hash=None,
        sandbox_strategy=None,
        capabilities=ResourceCapabilities(),
        created_at=now,
        updated_at=now,
    )
    _run_expecting_db_error(context, lambda: context.cov_res_repo.update(res))


@when("a resource is deleted and a database error is expected")
def step_res_delete_error(context: Context) -> None:
    _run_expecting_db_error(context, lambda: context.cov_res_repo.delete(_ulid()))


@when("a resource is resolved and a database error is expected")
def step_res_resolve_error(context: Context) -> None:
    _run_expecting_db_error(
        context,
        lambda: context.cov_res_repo.resolve_namespaced_name("broken/res"),
    )


# ══════════════════════════════════════════════════════════════════════════
# When steps — Resource Type Repository
# ══════════════════════════════════════════════════════════════════════════


@when("a resource type with equivalence metadata is created and retrieved")
def step_create_rt_with_equivalence(context: Context) -> None:
    from cleveragents.domain.models.core.resource_type import (
        ResourceKind,
        ResourceTypeSpec,
        SandboxStrategy,
    )

    spec = ResourceTypeSpec(
        name="bench/equiv-type",
        description="Type with equivalence",
        resource_kind=ResourceKind.PHYSICAL,
        sandbox_strategy=SandboxStrategy.NONE,
        user_addable=True,
        cli_args=[],
        parent_types=[],
        child_types=[],
        auto_discovery=None,
        equivalence={"key_fields": ["name", "location"], "strategy": "content_hash"},
        handler=None,
        built_in=False,
    )
    context.cov_rt_repo.create(spec)
    context.cov_session.commit()
    context.cov_retrieved_rt = context.cov_rt_repo.get("bench/equiv-type")


@when("a resource type is created and a database error is expected")
def step_rt_create_error(context: Context) -> None:
    from cleveragents.domain.models.core.resource_type import (
        ResourceKind,
        ResourceTypeSpec,
        SandboxStrategy,
    )

    spec = ResourceTypeSpec(
        name="broken/type",
        description="broken",
        resource_kind=ResourceKind.PHYSICAL,
        sandbox_strategy=SandboxStrategy.NONE,
        user_addable=True,
        cli_args=[],
        parent_types=[],
        child_types=[],
        auto_discovery=None,
        equivalence=None,
        handler=None,
        built_in=False,
    )
    _run_expecting_db_error(context, lambda: context.cov_rt_repo.create(spec))


@when("a resource type is retrieved and a database error is expected")
def step_rt_get_error(context: Context) -> None:
    _run_expecting_db_error(context, lambda: context.cov_rt_repo.get("broken/type"))


@when("resource types are listed and a database error is expected")
def step_rt_list_error(context: Context) -> None:
    _run_expecting_db_error(context, lambda: context.cov_rt_repo.list_types())


@when("a resource type is updated and a database error is expected")
def step_rt_update_error(context: Context) -> None:
    from cleveragents.domain.models.core.resource_type import (
        ResourceKind,
        ResourceTypeSpec,
        SandboxStrategy,
    )

    spec = ResourceTypeSpec(
        name="broken/type",
        description="broken",
        resource_kind=ResourceKind.PHYSICAL,
        sandbox_strategy=SandboxStrategy.NONE,
        user_addable=True,
        cli_args=[],
        parent_types=[],
        child_types=[],
        auto_discovery=None,
        equivalence=None,
        handler=None,
        built_in=False,
    )
    _run_expecting_db_error(context, lambda: context.cov_rt_repo.update(spec))


@when("a resource type is deleted and a database error is expected")
def step_rt_delete_error(context: Context) -> None:
    _run_expecting_db_error(context, lambda: context.cov_rt_repo.delete("broken/type"))


# ══════════════════════════════════════════════════════════════════════════
# Then steps
# ══════════════════════════════════════════════════════════════════════════


@then("the filtered session list should contain {count:d} entries")
def step_check_filtered_sessions(context: Context, count: int) -> None:
    assert len(context.cov_filtered_sessions) == count, (
        f"Expected {count}, got {len(context.cov_filtered_sessions)}"
    )


@then('a DatabaseError should be raised mentioning "{fragment}"')
def step_check_db_error(context: Context, fragment: str) -> None:
    assert context.cov_error is not None, (
        f"Expected a DatabaseError mentioning '{fragment}' but none was raised"
    )
    msg = str(context.cov_error)
    assert fragment.lower() in msg.lower(), (
        f"Expected '{fragment}' in error message, got: {msg}"
    )


@then("the attachment should store the arguments as JSON")
def step_check_attachment_args(context: Context) -> None:
    att = context.cov_attachment
    assert att is not None
    assert att["args_json"] is not None
    parsed = _json.loads(att["args_json"])
    assert parsed["severity"] == "error"
    assert parsed["max_depth"] == 5


@then("the attachment result should be empty")
def step_check_attachment_none(context: Context) -> None:
    assert context.cov_attachment_result is None


@then("the filtered resource list should contain {count:d} entry")
def step_check_filtered_resources(context: Context, count: int) -> None:
    assert len(context.cov_filtered_resources) == count, (
        f"Expected {count}, got {len(context.cov_filtered_resources)}"
    )


@then("the retrieved resource type should have the equivalence data")
def step_check_equivalence(context: Context) -> None:
    rt = context.cov_retrieved_rt
    assert rt is not None
    assert rt.equivalence is not None
    assert rt.equivalence["key_fields"] == ["name", "location"]
    assert rt.equivalence["strategy"] == "content_hash"
