"""Step definitions for cross_plan_correction_coverage_boost.feature.

Targets uncovered lines in cross_plan_correction_service.py:
  - Lines 60, 76, 92: Protocol method bodies (the ``...`` stubs)
  - Line 127: ``raise ValidationError`` for unrecognised ChildPlanState
  - Line 276: ``else []`` branch when rejection is None on rejected cascade
  - Lines 412-416: ``except`` handler in ``_rollback_completed_actions``
"""

from __future__ import annotations

from unittest.mock import MagicMock, patch

from behave import given, then, when

from cleveragents.application.services.cross_plan_correction_service import (
    ChildPlanCanceller,
    ChildPlanLookup,
    CrossPlanCorrectionService,
    SandboxRollbacker,
    classify_cascade_action,
)
from cleveragents.core.exceptions import ValidationError
from cleveragents.domain.models.core.correction import (
    CascadeAction,
    CascadeResult,
    ChildPlanState,
)

# -------------------------------------------------------------------
# Protocol-body subclasses — call super() to execute the ``...`` stub
# -------------------------------------------------------------------


class _LookupSubclass(ChildPlanLookup):
    """Calls super() to exercise ChildPlanLookup protocol body."""

    def get_child_plan_state(self, child_plan_id: str) -> ChildPlanState:
        result = super().get_child_plan_state(child_plan_id)  # type: ignore[safe-super]
        return result  # type: ignore[return-value]


class _CancellerSubclass(ChildPlanCanceller):
    """Calls super() to exercise ChildPlanCanceller protocol body."""

    def cancel_child_plan(self, child_plan_id: str) -> None:
        return super().cancel_child_plan(child_plan_id)  # type: ignore[safe-super]


class _RollbackerSubclass(SandboxRollbacker):
    """Calls super() to exercise SandboxRollbacker protocol body."""

    def rollback_child_plan_sandbox(self, child_plan_id: str) -> None:
        return super().rollback_child_plan_sandbox(child_plan_id)  # type: ignore[safe-super]


# -------------------------------------------------------------------
# Simple mock implementations for service construction
# -------------------------------------------------------------------


class _SimpleLookup:
    """Minimal lookup returning NOT_STARTED for any plan."""

    def get_child_plan_state(self, child_plan_id: str) -> ChildPlanState:
        return ChildPlanState.NOT_STARTED


class _SimpleCanceller:
    """Minimal canceller that records calls."""

    def __init__(self) -> None:
        self.cancelled: list[str] = []

    def cancel_child_plan(self, child_plan_id: str) -> None:
        self.cancelled.append(child_plan_id)


class _SimpleRollbacker:
    """Minimal rollbacker that records calls."""

    def __init__(self) -> None:
        self.rolled_back: list[str] = []

    def rollback_child_plan_sandbox(self, child_plan_id: str) -> None:
        self.rolled_back.append(child_plan_id)


# ===================================================================
# Protocol body coverage — lines 60, 76, 92
# ===================================================================


@when("I call ChildPlanLookup protocol body through a subclass")
def step_call_lookup_protocol_body(context: object) -> None:
    instance = _LookupSubclass()
    context.protocol_result = instance.get_child_plan_state("test-plan")  # type: ignore[attr-defined]


@then("the protocol body should return None")
def step_protocol_body_none(context: object) -> None:
    # The ``...`` (Ellipsis) expression is the protocol body.  It
    # evaluates as an expression statement and the method implicitly
    # returns None.
    assert context.protocol_result is None, (  # type: ignore[attr-defined]
        f"Expected None, got {context.protocol_result}"  # type: ignore[attr-defined]
    )


@when("I call ChildPlanCanceller protocol body through a subclass")
def step_call_canceller_protocol_body(context: object) -> None:
    instance = _CancellerSubclass()
    context.canceller_protocol_result = instance.cancel_child_plan("test-plan")  # type: ignore[attr-defined]


@then("the canceller protocol body should return None")
def step_canceller_protocol_body_none(context: object) -> None:
    assert context.canceller_protocol_result is None, (  # type: ignore[attr-defined]
        f"Expected None, got {context.canceller_protocol_result}"  # type: ignore[attr-defined]
    )


@when("I call SandboxRollbacker protocol body through a subclass")
def step_call_rollbacker_protocol_body(context: object) -> None:
    instance = _RollbackerSubclass()
    context.rollbacker_protocol_result = instance.rollback_child_plan_sandbox(
        "test-plan"
    )  # type: ignore[attr-defined]


@then("the rollbacker protocol body should return None")
def step_rollbacker_protocol_body_none(context: object) -> None:
    assert context.rollbacker_protocol_result is None, (  # type: ignore[attr-defined]
        f"Expected None, got {context.rollbacker_protocol_result}"  # type: ignore[attr-defined]
    )


# ===================================================================
# classify_cascade_action with unrecognised state — line 127
# ===================================================================


@when("I classify cascade action for an unrecognised state value")
def step_classify_unrecognised_state(context: object) -> None:
    try:
        # Pass a plain string that does not match any ChildPlanState member.
        classify_cascade_action("totally_unknown_state")  # type: ignore[arg-type]
        context.boost_error = None  # type: ignore[attr-defined]
    except ValidationError as exc:
        context.boost_error = exc  # type: ignore[attr-defined]


@then('a cascade ValidationError should be raised mentioning "{fragment}"')
def step_cascade_validation_error_with_fragment(context: object, fragment: str) -> None:
    assert context.boost_error is not None, (
        "Expected ValidationError but none was raised"
    )  # type: ignore[attr-defined]
    assert isinstance(context.boost_error, ValidationError), (  # type: ignore[attr-defined]
        f"Expected ValidationError, got {type(context.boost_error)}"  # type: ignore[attr-defined]
    )
    assert fragment in str(context.boost_error), (  # type: ignore[attr-defined]
        f"'{fragment}' not found in error message: {context.boost_error}"  # type: ignore[attr-defined]
    )


# ===================================================================
# execute_cascade — rejected=True, rejection=None — line 276
# ===================================================================


@given("a cross-plan correction service for coverage boost")
def step_create_boost_service(context: object) -> None:
    lookup = _SimpleLookup()
    canceller = _SimpleCanceller()
    rollbacker = _SimpleRollbacker()
    context.boost_lookup = lookup  # type: ignore[attr-defined]
    context.boost_canceller = canceller  # type: ignore[attr-defined]
    context.boost_rollbacker = rollbacker  # type: ignore[attr-defined]
    context.boost_service = CrossPlanCorrectionService(  # type: ignore[attr-defined]
        plan_lookup=lookup,
        plan_canceller=canceller,
        sandbox_rollbacker=rollbacker,
    )
    context.boost_cascade_result = None  # type: ignore[attr-defined]
    context.boost_error = None  # type: ignore[attr-defined]


@given("evaluate_cascade is patched to return rejected with no rejection object")
def step_patch_evaluate_cascade(context: object) -> None:
    """Prepare a patched evaluate_cascade that returns rejected=True
    but rejection=None, which normally never happens but the code
    defensively handles it (line 276 else branch)."""
    fake_result = CascadeResult(
        correction_id="C-BOOST",
        cascade_actions=[],
        rejected=True,
        rejection=None,
        all_cancelled_plan_ids=[],
        all_rolled_back_plan_ids=[],
    )
    context.boost_fake_result = fake_result  # type: ignore[attr-defined]


@when('I execute a cascade for correction "{cid}" with child plans "{plans}"')
def step_execute_cascade_boost(context: object, cid: str, plans: str) -> None:
    plan_ids = [p.strip() for p in plans.split(",")]
    service = context.boost_service  # type: ignore[attr-defined]

    # If a fake result was prepared, patch evaluate_cascade to return it
    if hasattr(context, "boost_fake_result") and context.boost_fake_result is not None:  # type: ignore[attr-defined]
        original_evaluate = service.evaluate_cascade

        def patched_evaluate(
            correction_id: str, affected_child_plan_ids: list[str]
        ) -> CascadeResult:
            return context.boost_fake_result  # type: ignore[attr-defined]

        service.evaluate_cascade = patched_evaluate  # type: ignore[attr-defined]
        try:
            context.boost_cascade_result = service.execute_cascade(cid, plan_ids)  # type: ignore[attr-defined]
        finally:
            service.evaluate_cascade = original_evaluate  # type: ignore[attr-defined]
    else:
        context.boost_cascade_result = service.execute_cascade(cid, plan_ids)  # type: ignore[attr-defined]


@then("the cascade result should be rejected with no rejection details")
def step_cascade_rejected_no_rejection(context: object) -> None:
    result = context.boost_cascade_result  # type: ignore[attr-defined]
    assert result is not None, "No cascade result captured"
    assert result.rejected is True, "Expected cascade to be rejected"
    assert result.rejection is None, (
        f"Expected rejection to be None, got {result.rejection}"
    )


# ===================================================================
# _rollback_completed_actions exception handler — lines 412-416
# ===================================================================


@given("the module logger info method is patched to raise an error")
def step_patch_logger_to_raise(context: object) -> None:
    """Mark that the logger should be patched to raise during rollback."""
    context.boost_patch_logger = True  # type: ignore[attr-defined]


@when("I call rollback_completed_actions with one completed action")
def step_call_rollback_with_failing_logger(context: object) -> None:
    service = context.boost_service  # type: ignore[attr-defined]
    action = CascadeAction(
        child_plan_id="CP-ROLLBACK-TEST",
        child_plan_state=ChildPlanState.NOT_STARTED,
        action="cancel",
        sandbox_rolled_back=False,
    )

    # Patch the module-level logger so that logger.info raises during
    # _rollback_completed_actions, hitting lines 412-416.
    mock_logger = MagicMock()
    mock_logger.info.side_effect = RuntimeError("Simulated logging failure")
    # logger.error must still work so the except block can log the error
    mock_logger.error = MagicMock()

    with patch(
        "cleveragents.application.services.cross_plan_correction_service.logger",
        mock_logger,
    ):
        try:
            service._rollback_completed_actions([action])
            context.boost_rollback_error = None  # type: ignore[attr-defined]
        except Exception as exc:
            context.boost_rollback_error = exc  # type: ignore[attr-defined]

    # Store the mock for assertion
    context.boost_mock_logger = mock_logger  # type: ignore[attr-defined]


@then("the rollback should complete without raising")
def step_rollback_no_raise(context: object) -> None:
    assert context.boost_rollback_error is None, (  # type: ignore[attr-defined]
        f"Expected no error, but got: {context.boost_rollback_error}"  # type: ignore[attr-defined]
    )
    # Verify the except handler called logger.error (line 413-416)
    mock_logger = context.boost_mock_logger  # type: ignore[attr-defined]
    assert mock_logger.error.called, (
        "Expected logger.error to be called in the except handler"
    )
    # Check that the error log included the expected event key
    call_args = mock_logger.error.call_args
    assert "cross_plan_correction.rollback_action_failed" in str(call_args), (
        f"Expected rollback_action_failed event, got: {call_args}"
    )
