"""Step definitions for correction_service_coverage_r2.feature.

Covers the exception-handling paths in ``execute_revert`` (lines 254-262)
and ``execute_append`` (lines 320-328) of
``cleveragents.application.services.correction_service.CorrectionService``.

These paths are unreachable under normal operation because the try-block
code is straightforward.  We use ``unittest.mock.patch`` to force
exceptions inside the try blocks so that the ``except Exception`` handlers
execute.

All step text uses a ``r2corr-`` prefix to avoid collisions with existing
step definitions in correction_service_coverage_steps.py and
correction_service_new_coverage_steps.py.
"""

from __future__ import annotations

from typing import Any
from unittest.mock import patch

from behave import given, then, when

from cleveragents.application.services.correction_service import CorrectionService
from cleveragents.domain.models.core.correction import (
    CorrectionMode,
    CorrectionStatus,
)

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


@given("r2corr-a fresh correction service")
def step_r2_background(context: Any) -> None:
    """Initialise a clean service and all context slots."""
    context.r2_svc = CorrectionService()
    context.r2_cid = None
    context.r2_revert_result = None
    context.r2_append_result = None
    context.r2_dispatch_result = None
    context.r2_patch = None


# -------------------------------------------------------------------
# Given - create corrections
# -------------------------------------------------------------------


@given('r2corr-a stored revert correction for plan "{plan}" targeting "{target}"')
def step_r2_stored_revert(context: Any, plan: str, target: str) -> None:
    req = context.r2_svc.request_correction(
        plan_id=plan,
        target_decision_id=target,
        mode=CorrectionMode.REVERT,
    )
    context.r2_cid = req.correction_id


@given('r2corr-a stored append correction for plan "{plan}" targeting "{target}"')
def step_r2_stored_append(context: Any, plan: str, target: str) -> None:
    req = context.r2_svc.request_correction(
        plan_id=plan,
        target_decision_id=target,
        mode=CorrectionMode.APPEND,
    )
    context.r2_cid = req.correction_id


# -------------------------------------------------------------------
# Given - monkeypatches to force exceptions
# -------------------------------------------------------------------


@given('r2corr-analyze_impact is patched to raise RuntimeError "{msg}"')
def step_r2_patch_analyze_runtime(context: Any, msg: str) -> None:
    """Replace ``analyze_impact`` with a function that raises RuntimeError."""
    original = context.r2_svc.analyze_impact

    def _boom(*args: Any, **kwargs: Any) -> None:
        raise RuntimeError(msg)

    context.r2_svc.analyze_impact = _boom  # type: ignore[assignment]
    context.r2_original_analyze = original


@given('r2corr-analyze_impact is patched to raise ValueError "{msg}"')
def step_r2_patch_analyze_value(context: Any, msg: str) -> None:
    """Replace ``analyze_impact`` with a function that raises ValueError."""

    def _boom(*args: Any, **kwargs: Any) -> None:
        raise ValueError(msg)

    context.r2_svc.analyze_impact = _boom  # type: ignore[assignment]


@given('r2corr-analyze_impact is patched to raise TypeError "{msg}"')
def step_r2_patch_analyze_type(context: Any, msg: str) -> None:
    """Replace ``analyze_impact`` with a function that raises TypeError."""

    def _boom(*args: Any, **kwargs: Any) -> None:
        raise TypeError(msg)

    context.r2_svc.analyze_impact = _boom  # type: ignore[assignment]


@given('r2corr-ULID is patched to raise RuntimeError "{msg}"')
def step_r2_patch_ulid_runtime(context: Any, msg: str) -> None:
    """Store patch info so the When step can apply it in the right scope."""
    context.r2_ulid_exc = RuntimeError(msg)


@given('r2corr-ULID is patched to raise ValueError "{msg}"')
def step_r2_patch_ulid_value(context: Any, msg: str) -> None:
    context.r2_ulid_exc = ValueError(msg)


@given('r2corr-ULID is patched to raise OSError "{msg}"')
def step_r2_patch_ulid_os(context: Any, msg: str) -> None:
    context.r2_ulid_exc = OSError(msg)


# -------------------------------------------------------------------
# When - execute_revert
# -------------------------------------------------------------------


@when("r2corr-I execute revert on the stored correction")
def step_r2_exec_revert(context: Any) -> None:
    """Execute revert; analyze_impact is already patched on the instance."""
    context.r2_revert_result = context.r2_svc.execute_revert(context.r2_cid, {})


# -------------------------------------------------------------------
# When - execute_append
# -------------------------------------------------------------------


@when("r2corr-I execute append on the stored correction")
def step_r2_exec_append(context: Any) -> None:
    """Execute append with ULID patched to raise."""
    exc = getattr(context, "r2_ulid_exc", None)
    if exc is not None:
        with patch(
            "cleveragents.application.services.correction_service.ULID",
            side_effect=exc,
        ):
            context.r2_append_result = context.r2_svc.execute_append(
                context.r2_cid,
            )
    else:
        context.r2_append_result = context.r2_svc.execute_append(
            context.r2_cid,
        )


# -------------------------------------------------------------------
# When - execute_correction (dispatch)
# -------------------------------------------------------------------


@when("r2corr-I dispatch execute correction on the stored correction")
def step_r2_dispatch(context: Any) -> None:
    """Dispatch execute_correction; patches are already active."""
    exc = getattr(context, "r2_ulid_exc", None)
    if exc is not None:
        with patch(
            "cleveragents.application.services.correction_service.ULID",
            side_effect=exc,
        ):
            context.r2_dispatch_result = context.r2_svc.execute_correction(
                context.r2_cid,
            )
    else:
        context.r2_dispatch_result = context.r2_svc.execute_correction(
            context.r2_cid,
            {},
        )


# -------------------------------------------------------------------
# Then - revert result assertions
# -------------------------------------------------------------------


@then('r2corr-the revert result status should be "{val}"')
def step_r2_revert_status(context: Any, val: str) -> None:
    assert context.r2_revert_result is not None, "No revert result captured"
    assert context.r2_revert_result.status.value == val, (
        f"Expected status '{val}', got '{context.r2_revert_result.status.value}'"
    )


@then('r2corr-the revert result error_message should contain "{fragment}"')
def step_r2_revert_error_msg(context: Any, fragment: str) -> None:
    msg = context.r2_revert_result.error_message
    assert msg is not None, "error_message is None"
    assert fragment in msg, f"'{fragment}' not found in error_message: '{msg}'"


# -------------------------------------------------------------------
# Then - append result assertions
# -------------------------------------------------------------------


@then('r2corr-the append result status should be "{val}"')
def step_r2_append_status(context: Any, val: str) -> None:
    assert context.r2_append_result is not None, "No append result captured"
    assert context.r2_append_result.status.value == val, (
        f"Expected status '{val}', got '{context.r2_append_result.status.value}'"
    )


@then('r2corr-the append result error_message should contain "{fragment}"')
def step_r2_append_error_msg(context: Any, fragment: str) -> None:
    msg = context.r2_append_result.error_message
    assert msg is not None, "error_message is None"
    assert fragment in msg, f"'{fragment}' not found in error_message: '{msg}'"


# -------------------------------------------------------------------
# Then - dispatch result assertions
# -------------------------------------------------------------------


@then('r2corr-the dispatch result status should be "{val}"')
def step_r2_dispatch_status(context: Any, val: str) -> None:
    assert context.r2_dispatch_result is not None, "No dispatch result captured"
    assert context.r2_dispatch_result.status.value == val, (
        f"Expected status '{val}', got '{context.r2_dispatch_result.status.value}'"
    )


@then('r2corr-the dispatch result error_message should contain "{fragment}"')
def step_r2_dispatch_error_msg(context: Any, fragment: str) -> None:
    msg = context.r2_dispatch_result.error_message
    assert msg is not None, "error_message is None"
    assert fragment in msg, f"'{fragment}' not found in error_message: '{msg}'"


# -------------------------------------------------------------------
# Then - stored correction status
# -------------------------------------------------------------------


@then('r2corr-the stored correction status should be "{val}"')
def step_r2_stored_status(context: Any, val: str) -> None:
    req = context.r2_svc.get_correction(context.r2_cid)
    assert req.status.value == val, (
        f"Expected stored status '{val}', got '{req.status.value}'"
    )


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


@then("r2corr-the first attempt should have success false")
def step_r2_attempt_fail(context: Any) -> None:
    attempts = context.r2_svc.list_attempts(context.r2_cid)
    assert len(attempts) > 0, "No attempts recorded"
    assert attempts[0].success is False, (
        f"Expected success=False, got {attempts[0].success}"
    )


@then("r2corr-the first attempt details should contain error key")
def step_r2_attempt_error_key(context: Any) -> None:
    attempts = context.r2_svc.list_attempts(context.r2_cid)
    assert "error" in attempts[0].details, (
        f"Expected 'error' key in details, got {attempts[0].details}"
    )


@then('r2corr-the first attempt details error should contain "{fragment}"')
def step_r2_attempt_error_val(context: Any, fragment: str) -> None:
    attempts = context.r2_svc.list_attempts(context.r2_cid)
    error_val = attempts[0].details.get("error", "")
    assert fragment in error_val, (
        f"'{fragment}' not found in attempt error: '{error_val}'"
    )


@then("r2corr-the first attempt completed_at should be set")
def step_r2_attempt_completed(context: Any) -> None:
    attempts = context.r2_svc.list_attempts(context.r2_cid)
    assert attempts[0].completed_at is not None, "completed_at is None"


@then("r2corr-the attempts for the stored correction should have {n:d} entry")
def step_r2_attempt_count(context: Any, n: int) -> None:
    attempts = context.r2_svc.list_attempts(context.r2_cid)
    assert len(attempts) == n, f"Expected {n} attempts, got {len(attempts)}"


# -------------------------------------------------------------------
# Then - result stored in service
# -------------------------------------------------------------------


@then("r2corr-the result should be stored in the service results dict")
def step_r2_result_stored(context: Any) -> None:
    assert context.r2_cid in context.r2_svc._results, (
        f"correction_id {context.r2_cid} not in _results"
    )
    stored = context.r2_svc._results[context.r2_cid]
    assert stored.status == CorrectionStatus.FAILED


@then("r2corr-the append result should be stored in the service results dict")
def step_r2_append_result_stored(context: Any) -> None:
    assert context.r2_cid in context.r2_svc._results, (
        f"correction_id {context.r2_cid} not in _results"
    )
    stored = context.r2_svc._results[context.r2_cid]
    assert stored.status == CorrectionStatus.FAILED
