"""Step definitions for db_repositories_cov_r3.feature.

Targets remaining uncovered lines in repositories.py across:
  - ActionRepository.list_all (lines 1005-1017)
  - NamespacedProjectRepository.get_context_policy (lines 2982-3012)
  - ToolRepository legacy wrappers (lines 3690-3717)
  - SessionRepository list_all / delete / update error paths (4063-4173)
  - SessionMessageRepository append / get / count (4227-4298)
  - AutomationProfileRepository._to_domain legacy safety fallback (4509-4514)
  - SkillRepository flattened tool cache methods (4851-5085)
  - DecisionRepository full CRUD (5153-5519)
  - CheckpointRepository full CRUD (5564-5717)
  - CorrectionAttemptRepository full CRUD + state (5789-6018)
"""

from __future__ import annotations

import hashlib
import json
from datetime import UTC, datetime, timedelta
from unittest.mock import MagicMock, patch

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

from cleveragents.core.exceptions import DatabaseError
from cleveragents.domain.models.core.checkpoint import Checkpoint, CheckpointMetadata
from cleveragents.domain.models.core.correction import (
    CorrectionAttemptRecord,
    CorrectionAttemptState,
    CorrectionMode,
)
from cleveragents.domain.models.core.decision import (
    ContextSnapshot,
    Decision,
    DecisionType,
)
from cleveragents.infrastructure.database.models import (
    AutomationProfileModel,
    Base,
    DecisionModel,
    LifecycleActionModel,
    LifecyclePlanModel,
    NamespacedProjectModel,
    SkillModel,
)
from cleveragents.infrastructure.database.repositories import (
    ActionRepository,
    AutomationProfileRepository,
    CheckpointRepository,
    CorrectionAttemptRepository,
    DecisionRepository,
    NamespacedProjectRepository,
    SessionMessageRepository,
    SessionRepository,
    SkillRepository,
    ToolNotFoundError,
    ToolRepository,
)

# ---------------------------------------------------------------------------
# ULID helper
# ---------------------------------------------------------------------------


def _next_ulid() -> str:
    return str(ULID())


# ---------------------------------------------------------------------------
# Database setup (shared Given)
# ---------------------------------------------------------------------------


@given("drcov3 an in-memory database with schema")
def step_setup_db(context: Context) -> None:
    if getattr(context, "drcov3_engine", None) is not None:
        return  # reuse within scenario

    engine = create_engine("sqlite:///:memory:", echo=False)
    Base.metadata.create_all(engine)
    factory = sessionmaker(bind=engine)
    context.drcov3_engine = engine
    context.drcov3_factory = factory
    context.drcov3_error = None
    context.drcov3_result = None


# ---------------------------------------------------------------------------
# Helpers: Insert ORM rows directly
# ---------------------------------------------------------------------------


def _ensure_action_row(session: Session, ns: str, name: str) -> None:
    """Insert a LifecycleActionModel row directly."""
    now = datetime.now(UTC).isoformat()
    nsname = f"{ns}/{name}"
    existing = (
        session.query(LifecycleActionModel).filter_by(namespaced_name=nsname).first()
    )
    if existing:
        return
    session.add(
        LifecycleActionModel(
            namespaced_name=nsname,
            namespace=ns,
            name=name,
            description="test action",
            definition_of_done="All tests pass",
            strategy_actor="default",
            execution_actor="default",
            state="available",
            reusable=True,
            read_only=False,
            created_by="test",
            tags_json="[]",
            created_at=now,
            updated_at=now,
        )
    )
    session.flush()


def _ensure_plan_row(session: Session, plan_id: str) -> None:
    """Insert a LifecyclePlanModel row if not exists."""
    existing = session.query(LifecyclePlanModel).filter_by(plan_id=plan_id).first()
    if existing:
        return
    _ensure_action_row(session, "local", "test-action")
    now = datetime.now(UTC).isoformat()
    session.add(
        LifecyclePlanModel(
            plan_id=plan_id,
            root_plan_id=plan_id,
            namespaced_name=f"local/test-plan-{plan_id[:8]}",
            namespace="local",
            action_name="local/test-action",
            description="Test plan",
            definition_of_done="Done",
            phase="action",
            processing_state="queued",
            attempt=1,
            reusable=False,
            read_only=False,
            created_by="test",
            tags_json="[]",
            effective_profile_snapshot="{}",
            created_at=now,
            updated_at=now,
        )
    )
    session.flush()


def _insert_decision_model(
    session: Session,
    plan_id: str,
    decision_id: str,
    sequence_number: int = 0,
    decision_type: str = "strategy_choice",
    parent_decision_id: str | None = None,
    superseded_by: str | None = None,
) -> None:
    now_iso = datetime.now(UTC).isoformat()
    session.add(
        DecisionModel(
            decision_id=decision_id,
            plan_id=plan_id,
            parent_decision_id=parent_decision_id,
            sequence_number=sequence_number,
            decision_type=decision_type,
            question="What approach?",
            chosen_option="Option A",
            context_snapshot_json=json.dumps(
                {
                    "hot_context_hash": "",
                    "hot_context_ref": "",
                    "relevant_resources": [],
                    "actor_state_ref": "",
                }
            ),
            rationale="",
            created_at=now_iso,
            is_correction=False,
            superseded_by=superseded_by,
        )
    )
    session.flush()


def _make_decision(
    plan_id: str,
    decision_id: str | None = None,
    parent_decision_id: str | None = None,
    sequence_number: int = 0,
    decision_type: DecisionType = DecisionType.STRATEGY_CHOICE,
    superseded_by: str | None = None,
) -> Decision:
    return Decision(
        decision_id=decision_id or _next_ulid(),
        plan_id=plan_id,
        parent_decision_id=parent_decision_id,
        sequence_number=sequence_number,
        decision_type=decision_type,
        question="What approach?",
        chosen_option="Option A",
        context_snapshot=ContextSnapshot(),
        superseded_by=superseded_by,
    )


def _make_checkpoint(
    plan_id: str,
    checkpoint_id: str | None = None,
    created_at: datetime | None = None,
) -> Checkpoint:
    return Checkpoint(
        checkpoint_id=checkpoint_id or _next_ulid(),
        plan_id=plan_id,
        sandbox_ref="abc123commit",
        checkpoint_type="manual",
        created_at=created_at or datetime.now(UTC),
        metadata=CheckpointMetadata(reason="test"),
    )


def _insert_skill_row(session: Session, name: str) -> None:
    """Insert a SkillModel row directly."""
    ns, short = name.split("/", 1)
    now = datetime.now(UTC).isoformat()
    existing = session.query(SkillModel).filter_by(name=name).first()
    if existing:
        return
    session.add(
        SkillModel(
            name=name,
            namespace=ns,
            short_name=short,
            description="test skill",
            version="1.0.0",
            created_at=now,
            updated_at=now,
        )
    )
    session.flush()


# ===================================================================
# ActionRepository.list_all
# ===================================================================


@given("drcov3 an ActionRepository instance")
def step_action_repo(context: Context) -> None:
    context.drcov3_action_repo = ActionRepository(
        session_factory=context.drcov3_factory,
    )


@given("drcov3 two actions exist in different namespaces")
def step_insert_two_actions(context: Context) -> None:
    session = context.drcov3_factory()
    _ensure_action_row(session, "beta", "action-b")
    _ensure_action_row(session, "alpha", "action-a")
    session.commit()
    session.close()


@when("drcov3 I call list_all on the action repository")
def step_call_list_all(context: Context) -> None:
    try:
        context.drcov3_result = context.drcov3_action_repo.list_all()
    except Exception as exc:
        context.drcov3_error = exc


@then("drcov3 it returns actions ordered by namespace then name")
def step_assert_ordered(context: Context) -> None:
    assert context.drcov3_error is None, f"Unexpected error: {context.drcov3_error}"
    result = context.drcov3_result
    assert isinstance(result, list)
    assert len(result) >= 2
    names = [str(getattr(a, "namespaced_name", getattr(a, "name", ""))) for a in result]
    assert names == sorted(names), f"Not ordered: {names}"


@given("drcov3 an ActionRepository with a broken session")
def step_broken_action_repo(context: Context) -> None:
    def broken_factory() -> Session:
        mock_session = MagicMock(spec=Session)
        mock_session.query.side_effect = OperationalError("db locked", {}, None)
        mock_session.rollback = MagicMock()
        mock_session.close = MagicMock()
        return mock_session  # type: ignore[return-value]

    context.drcov3_action_repo = ActionRepository(session_factory=broken_factory)


@when("drcov3 I call list_all expecting a DatabaseError")
def step_call_list_all_broken(context: Context) -> None:
    try:
        context.drcov3_action_repo.list_all()
    except DatabaseError as exc:
        context.drcov3_error = exc
    except Exception as exc:
        context.drcov3_error = exc


@then('drcov3 a DatabaseError was raised containing "{text}"')
def step_assert_db_error(context: Context, text: str) -> None:
    assert context.drcov3_error is not None, "Expected an error but none was raised"
    assert isinstance(context.drcov3_error, DatabaseError), (
        f"Expected DatabaseError, got {type(context.drcov3_error).__name__}: {context.drcov3_error}"
    )
    assert text in str(context.drcov3_error), (
        f"Expected '{text}' in error message, got: {context.drcov3_error}"
    )


# ===================================================================
# NamespacedProjectRepository.get_context_policy
# ===================================================================


@given("drcov3 a NamespacedProjectRepository instance")
def step_ns_project_repo(context: Context) -> None:
    context.drcov3_ns_project_repo = NamespacedProjectRepository(
        session_factory=context.drcov3_factory,
    )


@when('drcov3 I call get_context_policy for "{name}"')
def step_call_get_context_policy(context: Context, name: str) -> None:
    try:
        context.drcov3_result = context.drcov3_ns_project_repo.get_context_policy(name)
    except Exception as exc:
        context.drcov3_error = exc


@then("drcov3 the result is a default ProjectContextPolicy")
def step_assert_default_policy(context: Context) -> None:
    from cleveragents.domain.models.core.context_policy import ProjectContextPolicy

    assert context.drcov3_error is None, f"Unexpected error: {context.drcov3_error}"
    result = context.drcov3_result
    assert isinstance(result, ProjectContextPolicy)
    default = ProjectContextPolicy()
    assert result.model_dump() == default.model_dump()


@then("drcov3 the result is a valid ProjectContextPolicy")
def step_assert_valid_policy(context: Context) -> None:
    from cleveragents.domain.models.core.context_policy import ProjectContextPolicy

    assert context.drcov3_error is None, f"Unexpected error: {context.drcov3_error}"
    assert isinstance(context.drcov3_result, ProjectContextPolicy)


@given('drcov3 a project "{name}" exists with null context_policy_json')
def step_insert_project_null_policy(context: Context, name: str) -> None:
    session = context.drcov3_factory()
    ns = name.split("/")[0]
    now = datetime.now(UTC).isoformat()
    session.add(
        NamespacedProjectModel(
            namespaced_name=name,
            namespace=ns,
            description="test project",
            context_policy_json=None,
            tags_json="[]",
            created_at=now,
            updated_at=now,
        )
    )
    session.commit()
    session.close()


@given('drcov3 a project "{name}" exists with valid context_policy_json')
def step_insert_project_valid_policy(context: Context, name: str) -> None:
    session = context.drcov3_factory()
    ns = name.split("/")[0]
    now = datetime.now(UTC).isoformat()
    session.add(
        NamespacedProjectModel(
            namespaced_name=name,
            namespace=ns,
            description="test project",
            context_policy_json=json.dumps({}),
            tags_json="[]",
            created_at=now,
            updated_at=now,
        )
    )
    session.commit()
    session.close()


@given('drcov3 a project "{name}" exists with non-dict context_policy_json')
def step_insert_project_non_dict_policy(context: Context, name: str) -> None:
    session = context.drcov3_factory()
    ns = name.split("/")[0]
    now = datetime.now(UTC).isoformat()
    session.add(
        NamespacedProjectModel(
            namespaced_name=name,
            namespace=ns,
            description="test project",
            context_policy_json=json.dumps([1, 2, 3]),  # list, not dict
            tags_json="[]",
            created_at=now,
            updated_at=now,
        )
    )
    session.commit()
    session.close()


@given('drcov3 a project "{name}" exists with corrupt context_policy_json')
def step_insert_project_corrupt_policy(context: Context, name: str) -> None:
    session = context.drcov3_factory()
    ns = name.split("/")[0]
    now = datetime.now(UTC).isoformat()
    session.add(
        NamespacedProjectModel(
            namespaced_name=name,
            namespace=ns,
            description="test project",
            context_policy_json="{not valid json!!!",
            tags_json="[]",
            created_at=now,
            updated_at=now,
        )
    )
    session.commit()
    session.close()


@given("drcov3 a NamespacedProjectRepository with broken session")
def step_broken_ns_project_repo(context: Context) -> None:
    def broken_factory() -> Session:
        mock_session = MagicMock(spec=Session)
        mock_session.query.side_effect = OperationalError("db locked", {}, None)
        mock_session.rollback = MagicMock()
        mock_session.close = MagicMock()
        return mock_session  # type: ignore[return-value]

    context.drcov3_ns_project_repo = NamespacedProjectRepository(
        session_factory=broken_factory,
    )


@when("drcov3 I call get_context_policy expecting a DatabaseError")
def step_call_context_policy_broken(context: Context) -> None:
    try:
        context.drcov3_ns_project_repo.get_context_policy("ns/test")
    except DatabaseError as exc:
        context.drcov3_error = exc
    except Exception as exc:
        context.drcov3_error = exc


# ===================================================================
# ToolRepository legacy wrapper
# ===================================================================


@given("drcov3 a ToolRepository instance")
def step_tool_repo(context: Context) -> None:
    context.drcov3_tool_repo = ToolRepository(
        session_factory=context.drcov3_factory,
    )


@when('drcov3 I call ToolRepository get for "{name}"')
def step_tool_get(context: Context, name: str) -> None:
    try:
        context.drcov3_result = context.drcov3_tool_repo.get(name)
    except Exception as exc:
        context.drcov3_error = exc


@when('drcov3 I call ToolRepository get_by_name for "{name}"')
def step_tool_get_by_name(context: Context, name: str) -> None:
    try:
        context.drcov3_result = context.drcov3_tool_repo.get_by_name(name)
    except Exception as exc:
        context.drcov3_error = exc


@then("drcov3 the tool result is None")
def step_assert_tool_none(context: Context) -> None:
    assert context.drcov3_error is None, f"Unexpected error: {context.drcov3_error}"
    assert context.drcov3_result is None


@when('drcov3 I call ToolRepository remove for "{name}"')
def step_tool_remove(context: Context, name: str) -> None:
    try:
        context.drcov3_result = context.drcov3_tool_repo.remove(name)
    except ToolNotFoundError as exc:
        context.drcov3_error = exc
    except Exception as exc:
        context.drcov3_error = exc


@then("drcov3 a ToolNotFoundError was raised")
def step_assert_tool_not_found(context: Context) -> None:
    assert context.drcov3_error is not None, "Expected an error"
    assert isinstance(context.drcov3_error, ToolNotFoundError), (
        f"Expected ToolNotFoundError, got {type(context.drcov3_error).__name__}"
    )


@when("drcov3 I call ToolRepository update with a non-existent tool")
def step_tool_update_missing(context: Context) -> None:
    tool_dict = {
        "name": "ns/does-not-exist",
        "description": "nope",
        "tool_type": "tool",
        "source": "builtin",
    }
    try:
        context.drcov3_tool_repo.update(tool_dict)
    except ToolNotFoundError as exc:
        context.drcov3_error = exc
    except Exception as exc:
        context.drcov3_error = exc


# ===================================================================
# SessionRepository
# ===================================================================


@given("drcov3 a SessionRepository instance")
def step_session_repo(context: Context) -> None:
    context.drcov3_session_repo = SessionRepository(
        session_factory=context.drcov3_factory,
    )


@when("drcov3 I call session list_all")
def step_session_list_all(context: Context) -> None:
    try:
        context.drcov3_result = context.drcov3_session_repo.list_all()
    except Exception as exc:
        context.drcov3_error = exc


@then("drcov3 the session list result is a list")
def step_assert_session_list(context: Context) -> None:
    assert context.drcov3_error is None, f"Unexpected error: {context.drcov3_error}"
    assert isinstance(context.drcov3_result, list)


@given("drcov3 a SessionRepository with a broken session for delete")
def step_broken_session_repo_delete(context: Context) -> None:
    def broken_factory() -> Session:
        mock_session = MagicMock(spec=Session)
        mock_session.query.side_effect = OperationalError("disk I/O error", {}, None)
        mock_session.rollback = MagicMock()
        mock_session.close = MagicMock()
        return mock_session  # type: ignore[return-value]

    context.drcov3_session_repo = SessionRepository(
        session_factory=broken_factory,
    )


@when("drcov3 I call session delete expecting a DatabaseError")
def step_session_delete_broken(context: Context) -> None:
    try:
        context.drcov3_session_repo.delete("01FAKE_SESSION_ID0000000000")
    except DatabaseError as exc:
        context.drcov3_error = exc
    except Exception as exc:
        context.drcov3_error = exc


@given("drcov3 a SessionRepository with a broken session for update")
def step_broken_session_repo_update(context: Context) -> None:
    def broken_factory() -> Session:
        mock_session = MagicMock(spec=Session)
        mock_session.query.side_effect = OperationalError("disk I/O error", {}, None)
        mock_session.rollback = MagicMock()
        mock_session.close = MagicMock()
        return mock_session  # type: ignore[return-value]

    context.drcov3_session_repo = SessionRepository(
        session_factory=broken_factory,
    )


@when("drcov3 I call session update expecting a DatabaseError")
def step_session_update_broken(context: Context) -> None:
    fake_session = MagicMock()
    fake_session.session_id = "01FAKE_SESSION_ID0000000000"
    try:
        context.drcov3_session_repo.update(fake_session)
    except DatabaseError as exc:
        context.drcov3_error = exc
    except Exception as exc:
        context.drcov3_error = exc


# ===================================================================
# SessionMessageRepository
# ===================================================================


@given("drcov3 a SessionMessageRepository with a broken session")
def step_broken_msg_repo(context: Context) -> None:
    def broken_factory() -> Session:
        mock_session = MagicMock(spec=Session)
        mock_session.add.side_effect = OperationalError("disk I/O error", {}, None)
        mock_session.rollback = MagicMock()
        mock_session.close = MagicMock()
        return mock_session  # type: ignore[return-value]

    context.drcov3_msg_repo = SessionMessageRepository(
        session_factory=broken_factory,
    )


@when("drcov3 I call message append expecting a DatabaseError")
def step_msg_append_broken(context: Context) -> None:
    from cleveragents.infrastructure.database.models import SessionMessageModel

    fake_message = MagicMock()
    # Patch SessionMessageModel.from_domain to return a mock that triggers the error
    patcher = patch.object(
        SessionMessageModel,
        "from_domain",
        return_value=MagicMock(),
    )
    patcher.start()
    context.add_cleanup(patcher.stop)
    try:
        context.drcov3_msg_repo.append("01FAKE_SESSION", fake_message)
    except DatabaseError as exc:
        context.drcov3_error = exc
    except Exception as exc:
        context.drcov3_error = exc


@given("drcov3 a SessionMessageRepository instance")
def step_msg_repo(context: Context) -> None:
    context.drcov3_msg_repo = SessionMessageRepository(
        session_factory=context.drcov3_factory,
    )


@when("drcov3 I call count_for_session with a random session id")
def step_msg_count(context: Context) -> None:
    try:
        context.drcov3_result = context.drcov3_msg_repo.count_for_session(
            "01NONEXISTENT_SESSION_ID0000"
        )
    except Exception as exc:
        context.drcov3_error = exc


@then("drcov3 the count result is 0")
def step_assert_count_zero(context: Context) -> None:
    assert context.drcov3_error is None, f"Unexpected error: {context.drcov3_error}"
    assert context.drcov3_result == 0


# ===================================================================
# AutomationProfileRepository._to_domain legacy fallback
# ===================================================================


@given("drcov3 an AutomationProfileModel row without safety_json")
def step_profile_row_no_safety(context: Context) -> None:
    """Create a mock AutomationProfileModel with no safety_json."""
    row = MagicMock(spec=AutomationProfileModel)
    row.name = "test/profile"
    row.description = "Test profile"
    row.schema_version = "1.0.0"
    row.decompose_task = 0.5
    row.create_tool = 0.5
    row.select_tool = 0.5
    row.edit_code = 0.5
    row.execute_command = 0.5
    row.create_file = 0.5
    row.delete_content = 0.5
    row.access_network = 0.5
    row.install_dependency = 0.5
    row.modify_config = 0.5
    row.approve_plan = 0.5
    row.require_sandbox = True
    row.require_checkpoints = True
    row.allow_unsafe_tools = False
    row.guards_json = None
    # Crucially: safety_json is None to trigger the legacy fallback path
    row.safety_json = None
    context.drcov3_profile_row = row


@when("drcov3 I call _to_domain on the row")
def step_call_to_domain(context: Context) -> None:
    try:
        context.drcov3_result = AutomationProfileRepository._to_domain(
            context.drcov3_profile_row
        )
    except Exception as exc:
        context.drcov3_error = exc


@then("drcov3 the result has a SafetyProfile from legacy columns")
def step_assert_legacy_safety(context: Context) -> None:
    assert context.drcov3_error is None, f"Unexpected error: {context.drcov3_error}"
    profile = context.drcov3_result
    assert profile is not None
    safety = profile.safety
    assert safety is not None
    assert safety.require_sandbox is True
    assert safety.require_checkpoints is True


# ===================================================================
# SkillRepository flattened tool cache
# ===================================================================


@given("drcov3 a SkillRepository instance")
def step_skill_repo(context: Context) -> None:
    context.drcov3_skill_repo = SkillRepository(
        session_factory=context.drcov3_factory,
    )


@given('drcov3 a skill "{name}" exists')
def step_ensure_skill(context: Context, name: str) -> None:
    session = context.drcov3_factory()
    _insert_skill_row(session, name)
    session.commit()
    session.close()


@when('drcov3 I call skill delete for "{name}"')
def step_skill_delete(context: Context, name: str) -> None:
    try:
        context.drcov3_result = context.drcov3_skill_repo.delete(name)
    except Exception as exc:
        context.drcov3_error = exc


@then("drcov3 the skill delete result is True")
def step_assert_skill_deleted(context: Context) -> None:
    assert context.drcov3_error is None, f"Unexpected error: {context.drcov3_error}"
    assert context.drcov3_result is True


@when('drcov3 I call update_flattened_tools for "{name}"')
def step_update_flattened(context: Context, name: str) -> None:
    try:
        context.drcov3_skill_repo.update_flattened_tools(
            name=name,
            flattened_tools_json='[{"tool": "t1"}]',
            includes_json='["include1"]',
            capability_summary_json='{"cap": "summary"}',
            yaml_text="name: test\nitems: []",
            flattening_hash="abc123hash",
        )
        context.drcov3_result = "ok"
    except Exception as exc:
        context.drcov3_error = exc


@then("drcov3 the flattened tools are stored")
def step_assert_flattened_stored(context: Context) -> None:
    assert context.drcov3_error is None, f"Unexpected error: {context.drcov3_error}"
    assert context.drcov3_result == "ok"


@given('drcov3 a skill "{name}" exists with flattened tools')
def step_ensure_skill_with_flat(context: Context, name: str) -> None:
    session = context.drcov3_factory()
    _insert_skill_row(session, name)
    row = session.query(SkillModel).filter_by(name=name).first()
    row.flattened_tools_json = '[{"tool": "t1"}]'  # type: ignore[assignment]
    row.includes_json = '["include1"]'  # type: ignore[assignment]
    row.capability_summary_json = '{"cap": "summary"}'  # type: ignore[assignment]
    row.yaml_text = "name: test"  # type: ignore[assignment]
    row.flattening_hash = "stored_hash_abc"  # type: ignore[assignment]
    session.commit()
    session.close()


@when('drcov3 I call get_flattened_tools for "{name}"')
def step_get_flattened(context: Context, name: str) -> None:
    try:
        context.drcov3_result = context.drcov3_skill_repo.get_flattened_tools(name)
    except Exception as exc:
        context.drcov3_error = exc


@then("drcov3 the cache dict has expected keys")
def step_assert_cache_keys(context: Context) -> None:
    assert context.drcov3_error is None, f"Unexpected error: {context.drcov3_error}"
    result = context.drcov3_result
    assert isinstance(result, dict)
    expected_keys = {
        "flattened_tools_json",
        "includes_json",
        "capability_summary_json",
        "yaml_text",
        "flattening_hash",
    }
    assert set(result.keys()) == expected_keys
    assert result["flattened_tools_json"] is not None


@given('drcov3 a skill "{name}" exists with flattening_hash "{hash_val}"')
def step_ensure_skill_with_hash(context: Context, name: str, hash_val: str) -> None:
    session = context.drcov3_factory()
    _insert_skill_row(session, name)
    row = session.query(SkillModel).filter_by(name=name).first()
    row.flattening_hash = hash_val  # type: ignore[assignment]
    session.commit()
    session.close()


@when('drcov3 I call needs_refresh for "{name}" with hash "{hash_val}"')
def step_needs_refresh(context: Context, name: str, hash_val: str) -> None:
    try:
        context.drcov3_result = context.drcov3_skill_repo.needs_refresh(name, hash_val)
    except Exception as exc:
        context.drcov3_error = exc


@then("drcov3 the needs_refresh result is True")
def step_assert_needs_refresh(context: Context) -> None:
    assert context.drcov3_error is None, f"Unexpected error: {context.drcov3_error}"
    assert context.drcov3_result is True


@when('drcov3 I call recompute_flattening_hash for "{name}"')
def step_recompute_hash(context: Context, name: str) -> None:
    try:
        context.drcov3_result = context.drcov3_skill_repo.recompute_flattening_hash(
            name, "name: test\nitems: []"
        )
    except Exception as exc:
        context.drcov3_error = exc


@then("drcov3 the returned hash is a sha256 hex string")
def step_assert_sha256(context: Context) -> None:
    assert context.drcov3_error is None, f"Unexpected error: {context.drcov3_error}"
    result = context.drcov3_result
    assert isinstance(result, str)
    assert len(result) == 64  # sha256 hex digest length
    expected = hashlib.sha256(b"name: test\nitems: []").hexdigest()
    assert result == expected


@when('drcov3 I call invalidate_cached_summaries for "{name}"')
def step_invalidate_cached(context: Context, name: str) -> None:
    try:
        context.drcov3_skill_repo.invalidate_cached_summaries(name)
        context.drcov3_result = "ok"
    except Exception as exc:
        context.drcov3_error = exc


@then("drcov3 the cached fields are None")
def step_assert_cached_none(context: Context) -> None:
    assert context.drcov3_error is None, f"Unexpected error: {context.drcov3_error}"
    # Verify by reading back
    # We need the name — extract from the scenario context
    # Since we stored "ok", let's read from the same skill
    assert context.drcov3_result == "ok"


# ===================================================================
# DecisionRepository full CRUD
# ===================================================================


@given("drcov3 a DecisionRepository instance")
def step_decision_repo(context: Context) -> None:
    context.drcov3_decision_repo = DecisionRepository(
        session_factory=context.drcov3_factory,
    )


@given("drcov3 a plan exists for decisions")
def step_ensure_plan_for_decisions(context: Context) -> None:
    if not hasattr(context, "drcov3_plan_id"):
        context.drcov3_plan_id = _next_ulid()
    session = context.drcov3_factory()
    _ensure_plan_row(session, context.drcov3_plan_id)
    session.commit()
    session.close()


@when("drcov3 I create a decision")
def step_create_decision(context: Context) -> None:
    context.drcov3_decision_id = _next_ulid()
    decision = _make_decision(
        plan_id=context.drcov3_plan_id,
        decision_id=context.drcov3_decision_id,
    )
    try:
        context.drcov3_result = context.drcov3_decision_repo.create(decision)
    except Exception as exc:
        context.drcov3_error = exc


@when("drcov3 I get the decision by id")
def step_get_decision(context: Context) -> None:
    try:
        context.drcov3_retrieved = context.drcov3_decision_repo.get(
            context.drcov3_decision_id
        )
    except Exception as exc:
        context.drcov3_error = exc


@then("drcov3 the retrieved decision matches the created one")
def step_assert_decision_match(context: Context) -> None:
    assert context.drcov3_error is None, f"Unexpected error: {context.drcov3_error}"
    retrieved = context.drcov3_retrieved
    assert retrieved is not None
    assert retrieved.decision_id == context.drcov3_decision_id
    assert retrieved.plan_id == context.drcov3_plan_id


@given("drcov3 multiple decisions exist for the plan")
def step_insert_multiple_decisions(context: Context) -> None:
    session = context.drcov3_factory()
    context.drcov3_dec_ids = []
    for i in range(3):
        did = _next_ulid()
        context.drcov3_dec_ids.append(did)
        _insert_decision_model(
            session,
            context.drcov3_plan_id,
            did,
            sequence_number=i,
        )
    session.commit()
    session.close()


@when("drcov3 I call get_by_plan")
def step_get_by_plan(context: Context) -> None:
    try:
        context.drcov3_result = context.drcov3_decision_repo.get_by_plan(
            context.drcov3_plan_id
        )
    except Exception as exc:
        context.drcov3_error = exc


@then("drcov3 I get decisions ordered by sequence number")
def step_assert_ordered_decisions(context: Context) -> None:
    assert context.drcov3_error is None, f"Unexpected error: {context.drcov3_error}"
    result = context.drcov3_result
    assert isinstance(result, list)
    assert len(result) >= 3
    seq_nums = [d.sequence_number for d in result]
    assert seq_nums == sorted(seq_nums)


@given("drcov3 a decision tree exists")
def step_create_decision_tree(context: Context) -> None:
    session = context.drcov3_factory()
    root_id = _next_ulid()
    child_id = _next_ulid()
    leaf_id = _next_ulid()
    context.drcov3_root_id = root_id
    context.drcov3_child_id = child_id
    context.drcov3_leaf_id = leaf_id
    _insert_decision_model(session, context.drcov3_plan_id, root_id, 0)
    _insert_decision_model(
        session,
        context.drcov3_plan_id,
        child_id,
        1,
        parent_decision_id=root_id,
    )
    _insert_decision_model(
        session,
        context.drcov3_plan_id,
        leaf_id,
        2,
        parent_decision_id=child_id,
    )
    session.commit()
    session.close()


@when("drcov3 I call get_tree on the root")
def step_get_tree(context: Context) -> None:
    try:
        context.drcov3_result = context.drcov3_decision_repo.get_tree(
            context.drcov3_root_id
        )
    except Exception as exc:
        context.drcov3_error = exc


@then("drcov3 I get the root and its descendants in BFS order")
def step_assert_bfs(context: Context) -> None:
    assert context.drcov3_error is None, f"Unexpected error: {context.drcov3_error}"
    result = context.drcov3_result
    assert len(result) == 3
    assert result[0].decision_id == context.drcov3_root_id
    assert result[1].decision_id == context.drcov3_child_id
    assert result[2].decision_id == context.drcov3_leaf_id


@when("drcov3 I call get_path_to_root on a leaf")
def step_get_path_to_root(context: Context) -> None:
    try:
        context.drcov3_result = context.drcov3_decision_repo.get_path_to_root(
            context.drcov3_leaf_id
        )
    except Exception as exc:
        context.drcov3_error = exc


@then("drcov3 I get a path from leaf to root")
def step_assert_path_to_root(context: Context) -> None:
    assert context.drcov3_error is None, f"Unexpected error: {context.drcov3_error}"
    result = context.drcov3_result
    assert len(result) == 3
    assert result[0].decision_id == context.drcov3_leaf_id
    assert result[-1].decision_id == context.drcov3_root_id


@given("drcov3 a superseded decision exists")
def step_create_superseded(context: Context) -> None:
    session = context.drcov3_factory()
    old_id = _next_ulid()
    new_id = _next_ulid()
    context.drcov3_superseded_id = old_id
    context.drcov3_replacement_id = new_id
    _insert_decision_model(
        session,
        context.drcov3_plan_id,
        old_id,
        0,
        superseded_by=new_id,
    )
    _insert_decision_model(
        session,
        context.drcov3_plan_id,
        new_id,
        1,
    )
    session.commit()
    session.close()


@when("drcov3 I call get_superseded")
def step_get_superseded(context: Context) -> None:
    try:
        context.drcov3_result = context.drcov3_decision_repo.get_superseded(
            context.drcov3_plan_id
        )
    except Exception as exc:
        context.drcov3_error = exc


@then("drcov3 I get the superseded decisions")
def step_assert_superseded(context: Context) -> None:
    assert context.drcov3_error is None, f"Unexpected error: {context.drcov3_error}"
    result = context.drcov3_result
    assert len(result) >= 1
    ids = [d.decision_id for d in result]
    assert context.drcov3_superseded_id in ids


@given("drcov3 two decisions exist for supersede test")
def step_create_two_for_supersede(context: Context) -> None:
    session = context.drcov3_factory()
    d1 = _next_ulid()
    d2 = _next_ulid()
    context.drcov3_old_dec = d1
    context.drcov3_new_dec = d2
    _insert_decision_model(session, context.drcov3_plan_id, d1, 0)
    _insert_decision_model(session, context.drcov3_plan_id, d2, 1)
    session.commit()
    session.close()


@when("drcov3 I call update_superseded_by")
def step_update_superseded_by(context: Context) -> None:
    try:
        context.drcov3_result = context.drcov3_decision_repo.update_superseded_by(
            context.drcov3_old_dec, context.drcov3_new_dec
        )
    except Exception as exc:
        context.drcov3_error = exc


@then("drcov3 the decision has superseded_by set")
def step_assert_superseded_by(context: Context) -> None:
    assert context.drcov3_error is None, f"Unexpected error: {context.drcov3_error}"
    result = context.drcov3_result
    assert result.superseded_by == context.drcov3_new_dec


@given("drcov3 decisions of different types exist")
def step_create_different_types(context: Context) -> None:
    session = context.drcov3_factory()
    context.drcov3_strat_id = _next_ulid()
    context.drcov3_tool_id = _next_ulid()
    _insert_decision_model(
        session,
        context.drcov3_plan_id,
        context.drcov3_strat_id,
        0,
        decision_type="strategy_choice",
    )
    _insert_decision_model(
        session,
        context.drcov3_plan_id,
        context.drcov3_tool_id,
        1,
        decision_type="tool_invocation",
    )
    session.commit()
    session.close()


@when("drcov3 I call list_by_type for strategy_choice")
def step_list_by_type(context: Context) -> None:
    try:
        context.drcov3_result = context.drcov3_decision_repo.list_by_type(
            context.drcov3_plan_id, "strategy_choice"
        )
    except Exception as exc:
        context.drcov3_error = exc


@then("drcov3 I only get strategy_choice decisions")
def step_assert_type_filter(context: Context) -> None:
    assert context.drcov3_error is None, f"Unexpected error: {context.drcov3_error}"
    result = context.drcov3_result
    assert len(result) >= 1
    for d in result:
        assert d.decision_type == DecisionType.STRATEGY_CHOICE


@given("drcov3 a single decision exists for delete test")
def step_create_for_delete(context: Context) -> None:
    session = context.drcov3_factory()
    did = _next_ulid()
    context.drcov3_del_dec_id = did
    _insert_decision_model(session, context.drcov3_plan_id, did, 0)
    session.commit()
    session.close()


@when("drcov3 I delete the decision")
def step_delete_decision(context: Context) -> None:
    try:
        context.drcov3_result = context.drcov3_decision_repo.delete(
            context.drcov3_del_dec_id
        )
    except Exception as exc:
        context.drcov3_error = exc


@then("drcov3 the delete result is True")
def step_assert_delete_true(context: Context) -> None:
    assert context.drcov3_error is None, f"Unexpected error: {context.drcov3_error}"
    assert context.drcov3_result is True


@when("drcov3 I call get_max_sequence_number")
def step_get_max_seq(context: Context) -> None:
    try:
        context.drcov3_result = context.drcov3_decision_repo.get_max_sequence_number(
            context.drcov3_plan_id
        )
    except Exception as exc:
        context.drcov3_error = exc


@then("drcov3 the max sequence number is correct")
def step_assert_max_seq(context: Context) -> None:
    assert context.drcov3_error is None, f"Unexpected error: {context.drcov3_error}"
    assert context.drcov3_result is not None
    assert context.drcov3_result >= 2  # at least 0, 1, 2


@when("drcov3 I call decision count")
def step_decision_count(context: Context) -> None:
    try:
        context.drcov3_result = context.drcov3_decision_repo.count(
            context.drcov3_plan_id
        )
    except Exception as exc:
        context.drcov3_error = exc


@then("drcov3 the decision count matches expected")
def step_assert_decision_count(context: Context) -> None:
    assert context.drcov3_error is None, f"Unexpected error: {context.drcov3_error}"
    assert context.drcov3_result >= 3


# ===================================================================
# CheckpointRepository full CRUD
# ===================================================================


@given("drcov3 a CheckpointRepository instance")
def step_checkpoint_repo(context: Context) -> None:
    context.drcov3_ckpt_repo = CheckpointRepository(
        session_factory=context.drcov3_factory,
    )


@given("drcov3 a plan exists for checkpoints")
def step_ensure_plan_for_ckpts(context: Context) -> None:
    if not hasattr(context, "drcov3_ckpt_plan_id"):
        context.drcov3_ckpt_plan_id = _next_ulid()
    session = context.drcov3_factory()
    _ensure_plan_row(session, context.drcov3_ckpt_plan_id)
    session.commit()
    session.close()


@when("drcov3 I create a checkpoint")
def step_create_ckpt(context: Context) -> None:
    context.drcov3_ckpt_id = _next_ulid()
    ckpt = _make_checkpoint(
        plan_id=context.drcov3_ckpt_plan_id,
        checkpoint_id=context.drcov3_ckpt_id,
    )
    try:
        context.drcov3_result = context.drcov3_ckpt_repo.create(ckpt)
    except Exception as exc:
        context.drcov3_error = exc


@when("drcov3 I get the checkpoint by id")
def step_get_ckpt(context: Context) -> None:
    try:
        context.drcov3_retrieved = context.drcov3_ckpt_repo.get_by_id(
            context.drcov3_ckpt_id
        )
    except Exception as exc:
        context.drcov3_error = exc


@then("drcov3 the retrieved checkpoint matches")
def step_assert_ckpt_match(context: Context) -> None:
    assert context.drcov3_error is None, f"Unexpected error: {context.drcov3_error}"
    retrieved = context.drcov3_retrieved
    assert retrieved is not None
    assert retrieved.checkpoint_id == context.drcov3_ckpt_id


@given("drcov3 multiple checkpoints exist for the plan")
def step_insert_multi_ckpts(context: Context) -> None:
    context.drcov3_ckpt_ids = []
    for i in range(3):
        cid = _next_ulid()
        context.drcov3_ckpt_ids.append(cid)
        ckpt = _make_checkpoint(
            plan_id=context.drcov3_ckpt_plan_id,
            checkpoint_id=cid,
            created_at=datetime.now(UTC) + timedelta(seconds=i),
        )
        context.drcov3_ckpt_repo.create(ckpt)


@when("drcov3 I call checkpoint list_by_plan")
def step_list_ckpts(context: Context) -> None:
    try:
        context.drcov3_result = context.drcov3_ckpt_repo.list_by_plan(
            context.drcov3_ckpt_plan_id
        )
    except Exception as exc:
        context.drcov3_error = exc


@then("drcov3 I get checkpoints ordered by creation time")
def step_assert_ckpts_ordered(context: Context) -> None:
    assert context.drcov3_error is None, f"Unexpected error: {context.drcov3_error}"
    result = context.drcov3_result
    assert isinstance(result, list)
    assert len(result) >= 3
    timestamps = [c.created_at for c in result]
    assert timestamps == sorted(timestamps)


@given("drcov3 a checkpoint exists for delete test")
def step_create_ckpt_for_delete(context: Context) -> None:
    context.drcov3_del_ckpt_id = _next_ulid()
    ckpt = _make_checkpoint(
        plan_id=context.drcov3_ckpt_plan_id,
        checkpoint_id=context.drcov3_del_ckpt_id,
    )
    context.drcov3_ckpt_repo.create(ckpt)


@when("drcov3 I delete the checkpoint")
def step_delete_ckpt(context: Context) -> None:
    try:
        context.drcov3_result = context.drcov3_ckpt_repo.delete(
            context.drcov3_del_ckpt_id
        )
    except Exception as exc:
        context.drcov3_error = exc


@then("drcov3 the checkpoint delete result is True")
def step_assert_ckpt_deleted(context: Context) -> None:
    assert context.drcov3_error is None, f"Unexpected error: {context.drcov3_error}"
    assert context.drcov3_result is True


@given("drcov3 five checkpoints exist for prune test")
def step_create_five_ckpts(context: Context) -> None:
    context.drcov3_prune_ckpt_ids = []
    for i in range(5):
        cid = _next_ulid()
        context.drcov3_prune_ckpt_ids.append(cid)
        ckpt = _make_checkpoint(
            plan_id=context.drcov3_ckpt_plan_id,
            checkpoint_id=cid,
            created_at=datetime.now(UTC) + timedelta(seconds=i),
        )
        context.drcov3_ckpt_repo.create(ckpt)


@when("drcov3 I call prune with max_checkpoints 3")
def step_prune_ckpts(context: Context) -> None:
    try:
        context.drcov3_result = context.drcov3_ckpt_repo.prune(
            context.drcov3_ckpt_plan_id, max_checkpoints=3
        )
    except Exception as exc:
        context.drcov3_error = exc


@then("drcov3 two interior checkpoints are removed")
def step_assert_pruned(context: Context) -> None:
    assert context.drcov3_error is None, f"Unexpected error: {context.drcov3_error}"
    result = context.drcov3_result
    assert isinstance(result, list)
    assert len(result) == 2
    # Pruned IDs should be interior ones (not first or last)
    assert context.drcov3_prune_ckpt_ids[0] not in result
    assert context.drcov3_prune_ckpt_ids[-1] not in result


# ===================================================================
# CorrectionAttemptRepository full CRUD
# ===================================================================


@given("drcov3 a CorrectionAttemptRepository instance")
def step_correction_repo(context: Context) -> None:
    context.drcov3_corr_repo = CorrectionAttemptRepository(
        session_factory=context.drcov3_factory,
    )


@given("drcov3 a plan and decision exist for corrections")
def step_ensure_plan_decision(context: Context) -> None:
    if not hasattr(context, "drcov3_corr_plan_id"):
        context.drcov3_corr_plan_id = _next_ulid()
    if not hasattr(context, "drcov3_corr_decision_id"):
        context.drcov3_corr_decision_id = _next_ulid()
    session = context.drcov3_factory()
    _ensure_plan_row(session, context.drcov3_corr_plan_id)
    # Insert a decision for the FK
    existing = (
        session.query(DecisionModel)
        .filter_by(decision_id=context.drcov3_corr_decision_id)
        .first()
    )
    if not existing:
        _insert_decision_model(
            session,
            context.drcov3_corr_plan_id,
            context.drcov3_corr_decision_id,
            0,
        )
    session.commit()
    session.close()


@when("drcov3 I create a correction attempt")
def step_create_correction(context: Context) -> None:
    context.drcov3_corr_id = _next_ulid()
    record = CorrectionAttemptRecord(
        correction_attempt_id=context.drcov3_corr_id,
        plan_id=context.drcov3_corr_plan_id,
        original_decision_id=context.drcov3_corr_decision_id,
        mode=CorrectionMode.REVERT,
        guidance="Fix the issue",
        state=CorrectionAttemptState.PENDING,
    )
    try:
        context.drcov3_result = context.drcov3_corr_repo.create(record)
    except Exception as exc:
        context.drcov3_error = exc


@when("drcov3 I get the correction attempt by id")
def step_get_correction(context: Context) -> None:
    try:
        context.drcov3_retrieved = context.drcov3_corr_repo.get(context.drcov3_corr_id)
    except Exception as exc:
        context.drcov3_error = exc


@then("drcov3 the retrieved correction attempt matches")
def step_assert_correction_match(context: Context) -> None:
    assert context.drcov3_error is None, f"Unexpected error: {context.drcov3_error}"
    retrieved = context.drcov3_retrieved
    assert retrieved is not None
    assert retrieved.correction_attempt_id == context.drcov3_corr_id
    assert retrieved.plan_id == context.drcov3_corr_plan_id


@given("drcov3 a correction attempt exists for list test")
def step_create_correction_for_list(context: Context) -> None:
    context.drcov3_list_corr_id = _next_ulid()
    record = CorrectionAttemptRecord(
        correction_attempt_id=context.drcov3_list_corr_id,
        plan_id=context.drcov3_corr_plan_id,
        original_decision_id=context.drcov3_corr_decision_id,
        mode=CorrectionMode.APPEND,
        guidance="Append fix",
        state=CorrectionAttemptState.PENDING,
    )
    context.drcov3_corr_repo.create(record)


@when("drcov3 I call correction list_by_plan")
def step_list_corrections(context: Context) -> None:
    try:
        context.drcov3_result = context.drcov3_corr_repo.list_by_plan(
            context.drcov3_corr_plan_id
        )
    except Exception as exc:
        context.drcov3_error = exc


@then("drcov3 the correction list contains the attempt")
def step_assert_correction_list(context: Context) -> None:
    assert context.drcov3_error is None, f"Unexpected error: {context.drcov3_error}"
    result = context.drcov3_result
    assert isinstance(result, list)
    assert len(result) >= 1
    ids = [r.correction_attempt_id for r in result]
    assert context.drcov3_list_corr_id in ids


@given("drcov3 a pending correction attempt exists for state test")
def step_create_pending_correction(context: Context) -> None:
    context.drcov3_state_corr_id = _next_ulid()
    record = CorrectionAttemptRecord(
        correction_attempt_id=context.drcov3_state_corr_id,
        plan_id=context.drcov3_corr_plan_id,
        original_decision_id=context.drcov3_corr_decision_id,
        mode=CorrectionMode.REVERT,
        guidance="Pending correction",
        state=CorrectionAttemptState.PENDING,
    )
    context.drcov3_corr_repo.create(record)


@when("drcov3 I update correction state to executing")
def step_update_state_executing(context: Context) -> None:
    try:
        context.drcov3_result = context.drcov3_corr_repo.update_state(
            context.drcov3_state_corr_id,
            CorrectionAttemptState.EXECUTING,
        )
    except Exception as exc:
        context.drcov3_error = exc


@then("drcov3 the correction state is executing")
def step_assert_executing(context: Context) -> None:
    assert context.drcov3_error is None, f"Unexpected error: {context.drcov3_error}"
    result = context.drcov3_result
    assert result.state == CorrectionAttemptState.EXECUTING


@given("drcov3 a correction attempt exists for delete test")
def step_create_correction_for_delete(context: Context) -> None:
    context.drcov3_del_corr_id = _next_ulid()
    record = CorrectionAttemptRecord(
        correction_attempt_id=context.drcov3_del_corr_id,
        plan_id=context.drcov3_corr_plan_id,
        original_decision_id=context.drcov3_corr_decision_id,
        mode=CorrectionMode.REVERT,
        guidance="To be deleted",
        state=CorrectionAttemptState.PENDING,
    )
    context.drcov3_corr_repo.create(record)


@when("drcov3 I delete the correction attempt")
def step_delete_correction(context: Context) -> None:
    try:
        context.drcov3_result = context.drcov3_corr_repo.delete(
            context.drcov3_del_corr_id
        )
    except Exception as exc:
        context.drcov3_error = exc


@then("drcov3 the correction delete result is True")
def step_assert_correction_deleted(context: Context) -> None:
    assert context.drcov3_error is None, f"Unexpected error: {context.drcov3_error}"
    assert context.drcov3_result is True


# -- CorrectionAttemptRepository error paths --


@given("drcov3 a CorrectionAttemptRepository with broken session")
def step_broken_correction_repo_create(context: Context) -> None:
    def broken_factory() -> Session:
        mock_session = MagicMock(spec=Session)
        mock_session.add.side_effect = OperationalError("disk I/O error", {}, None)
        mock_session.rollback = MagicMock()
        mock_session.close = MagicMock()
        mock_session.flush = MagicMock()
        return mock_session  # type: ignore[return-value]

    context.drcov3_corr_repo = CorrectionAttemptRepository(
        session_factory=broken_factory,
    )


@when("drcov3 I call correction create expecting a DatabaseError")
def step_correction_create_broken(context: Context) -> None:
    from cleveragents.infrastructure.database.models import (
        CorrectionAttemptModel as CAM,
    )

    patcher = patch.object(
        CAM,
        "from_domain",
        return_value=MagicMock(),
    )
    patcher.start()
    context.add_cleanup(patcher.stop)

    record = CorrectionAttemptRecord(
        correction_attempt_id=_next_ulid(),
        plan_id=_next_ulid(),
        original_decision_id=_next_ulid(),
        mode=CorrectionMode.REVERT,
        guidance="test",
        state=CorrectionAttemptState.PENDING,
    )
    try:
        context.drcov3_corr_repo.create(record)
    except DatabaseError as exc:
        context.drcov3_error = exc
    except Exception as exc:
        context.drcov3_error = exc


@given("drcov3 a CorrectionAttemptRepository with broken session for get")
def step_broken_correction_repo_get(context: Context) -> None:
    def broken_factory() -> Session:
        mock_session = MagicMock(spec=Session)
        mock_session.query.side_effect = OperationalError("disk I/O error", {}, None)
        mock_session.rollback = MagicMock()
        mock_session.close = MagicMock()
        return mock_session  # type: ignore[return-value]

    context.drcov3_corr_repo = CorrectionAttemptRepository(
        session_factory=broken_factory,
    )


@when("drcov3 I call correction get expecting a DatabaseError")
def step_correction_get_broken(context: Context) -> None:
    try:
        context.drcov3_corr_repo.get("01NONEXISTENT0000000000000")
    except DatabaseError as exc:
        context.drcov3_error = exc
    except Exception as exc:
        context.drcov3_error = exc


@given("drcov3 a CorrectionAttemptRepository with broken session for list")
def step_broken_correction_repo_list(context: Context) -> None:
    def broken_factory() -> Session:
        mock_session = MagicMock(spec=Session)
        mock_session.query.side_effect = OperationalError("disk I/O error", {}, None)
        mock_session.rollback = MagicMock()
        mock_session.close = MagicMock()
        return mock_session  # type: ignore[return-value]

    context.drcov3_corr_repo = CorrectionAttemptRepository(
        session_factory=broken_factory,
    )


@when("drcov3 I call correction list_by_plan expecting a DatabaseError")
def step_correction_list_broken(context: Context) -> None:
    try:
        context.drcov3_corr_repo.list_by_plan("01FAKE_PLAN_ID000000000000")
    except DatabaseError as exc:
        context.drcov3_error = exc
    except Exception as exc:
        context.drcov3_error = exc


@given("drcov3 a CorrectionAttemptRepository with broken session for delete")
def step_broken_correction_repo_delete(context: Context) -> None:
    def broken_factory() -> Session:
        mock_session = MagicMock(spec=Session)
        mock_session.query.side_effect = OperationalError("disk I/O error", {}, None)
        mock_session.rollback = MagicMock()
        mock_session.close = MagicMock()
        return mock_session  # type: ignore[return-value]

    context.drcov3_corr_repo = CorrectionAttemptRepository(
        session_factory=broken_factory,
    )


@when("drcov3 I call correction delete expecting a DatabaseError")
def step_correction_delete_broken(context: Context) -> None:
    try:
        context.drcov3_corr_repo.delete("01FAKE_CORR_ID0000000000000")
    except DatabaseError as exc:
        context.drcov3_error = exc
    except Exception as exc:
        context.drcov3_error = exc


@given("drcov3 a CorrectionAttemptRepository with broken session for update_state")
def step_broken_correction_repo_update_state(context: Context) -> None:
    def broken_factory() -> Session:
        mock_session = MagicMock(spec=Session)
        mock_session.query.side_effect = OperationalError("disk I/O error", {}, None)
        mock_session.rollback = MagicMock()
        mock_session.close = MagicMock()
        return mock_session  # type: ignore[return-value]

    context.drcov3_corr_repo = CorrectionAttemptRepository(
        session_factory=broken_factory,
    )


@when("drcov3 I call correction update_state expecting a DatabaseError")
def step_correction_update_state_broken(context: Context) -> None:
    try:
        context.drcov3_corr_repo.update_state(
            "01FAKE_CORR_ID0000000000000",
            CorrectionAttemptState.EXECUTING,
        )
    except DatabaseError as exc:
        context.drcov3_error = exc
    except Exception as exc:
        context.drcov3_error = exc
