"""Step definitions for plan_apply_service_branch_coverage feature.

Targets the missed branch at line 426→432 in plan_apply_service.py.
When self._logger.error() raises an exception inside handle_merge_failure,
execution skips the ``return plan`` statement and exits via exception
propagation.  This file exercises both the normal (return) and exception
(propagation) paths.
"""

from __future__ import annotations

from unittest.mock import MagicMock

from behave import given, then, when  # type: ignore[import-untyped]
from behave.runner import Context  # type: ignore[import-untyped]

from cleveragents.application.services.plan_apply_service import (
    PlanApplyService,
)
from cleveragents.domain.models.core.plan import (
    PlanPhase,
    PlanTimestamps,
    ProcessingState,
)

__all__: list[str] = []

# Plan / changeset IDs used across scenarios
_PLAN_ID = "01BRANCHTEST000000000001"


# ----------------------------------------------------------------------
# Helpers
# ----------------------------------------------------------------------


class _StubPhase:
    """Lightweight stub that mirrors PlanPhase comparison."""

    def __init__(self, phase: PlanPhase) -> None:
        self._phase = phase
        self.value = phase.value

    def __eq__(self, other: object) -> bool:
        if isinstance(other, PlanPhase):
            return self._phase == other
        if isinstance(other, _StubPhase):
            return self._phase == other._phase
        return NotImplemented


class _StubState:
    """Lightweight stub that mirrors ProcessingState comparison."""

    def __init__(self, state: ProcessingState) -> None:
        self._state = state
        self.value = state.value

    def __eq__(self, other: object) -> bool:
        if isinstance(other, ProcessingState):
            return self._state == other
        if isinstance(other, _StubState):
            return self._state == other._state
        return NotImplemented


def _make_mock_plan(
    *,
    plan_id: str = _PLAN_ID,
    phase: PlanPhase = PlanPhase.EXECUTE,
    state: ProcessingState = ProcessingState.COMPLETE,
    error_details: dict[str, str] | None = None,
) -> MagicMock:
    """Create a mock Plan with sensible defaults."""
    plan = MagicMock()
    plan.identity.plan_id = plan_id
    plan.phase = _StubPhase(phase)
    plan.processing_state = _StubState(state)
    plan.changeset_id = None
    plan.validation_summary = None
    plan.is_terminal = False
    plan.error_details = error_details
    plan.timestamps = PlanTimestamps()
    plan.sandbox_refs = ["sandbox-ref-branch"]
    return plan


def _make_lifecycle_mock(plan: MagicMock) -> MagicMock:
    """Create a mock PlanLifecycleService that returns *plan*."""
    lifecycle = MagicMock()
    lifecycle.get_plan.return_value = plan
    lifecycle._commit_plan = MagicMock()
    lifecycle.fail_apply = MagicMock(return_value=plan)
    return lifecycle


# ======================================================================
# Scenario: logger.error raises → exception propagates (branch 426→432)
# ======================================================================


@given("pas_branch a service whose logger.error will raise RuntimeError")
def step_service_logger_error_raises(context: Context) -> None:
    """Build a PlanApplyService and patch _logger.error to raise."""
    plan = _make_mock_plan(error_details=None)
    context.branch_plan = plan

    lifecycle = _make_lifecycle_mock(plan)
    context.branch_lifecycle = lifecycle

    service = PlanApplyService(
        lifecycle_service=lifecycle,
        changeset_store=None,
    )

    # Replace the logger's .error method so it raises
    mock_logger = MagicMock()
    mock_logger.error.side_effect = RuntimeError("simulated logger failure")
    # Keep other logger methods working
    mock_logger.info = MagicMock()
    mock_logger.warning = MagicMock()
    mock_logger.debug = MagicMock()
    service._logger = mock_logger

    context.branch_service = service
    context.branch_mock_logger = mock_logger


@when("pas_branch I call handle_merge_failure and capture any exception")
def step_call_handle_merge_failure_capture(context: Context) -> None:
    """Call handle_merge_failure and capture any raised exception."""
    context.branch_raised_error = None
    context.branch_returned_plan = None
    try:
        context.branch_returned_plan = context.branch_service.handle_merge_failure(
            plan_id=_PLAN_ID,
            conflict_details="conflicting changes in main.py",
        )
    except RuntimeError as exc:
        context.branch_raised_error = exc


@then("pas_branch a RuntimeError should have been raised")
def step_runtime_error_raised(context: Context) -> None:
    """Assert that a RuntimeError was raised from the logger."""
    assert context.branch_raised_error is not None, (
        "Expected RuntimeError to propagate from handle_merge_failure, "
        "but no exception was raised"
    )
    assert isinstance(context.branch_raised_error, RuntimeError), (
        f"Expected RuntimeError, got {type(context.branch_raised_error).__name__}"
    )
    assert "simulated logger failure" in str(context.branch_raised_error)


@then("pas_branch the plan error_details should still contain merge_conflict")
def step_plan_has_merge_conflict(context: Context) -> None:
    """Assert error_details were set before the logger blew up."""
    details = context.branch_plan.error_details
    assert details is not None, "error_details should have been set before the error"
    assert "merge_conflict" in details, (
        f"Expected 'merge_conflict' in error_details, got keys: {list(details.keys())}"
    )
    assert details["merge_conflict"] == "conflicting changes in main.py"
    assert details["sandbox_rollback"] == "pending"


@then("pas_branch lifecycle _commit_plan should have been invoked before the error")
def step_commit_plan_invoked(context: Context) -> None:
    """Assert _commit_plan was called (it runs before the logger call)."""
    context.branch_lifecycle._commit_plan.assert_called_once()


@then("pas_branch lifecycle fail_apply should have been invoked before the error")
def step_fail_apply_invoked(context: Context) -> None:
    """Assert fail_apply was called (it runs before the logger call)."""
    context.branch_lifecycle.fail_apply.assert_called_once_with(
        _PLAN_ID,
        "Merge failed: conflicting changes in main.py",
    )


# ======================================================================
# Scenario: logger.error succeeds → normal return (branch 426→431)
# ======================================================================


@given("pas_branch a service whose logger.error will succeed normally")
def step_service_logger_error_succeeds(context: Context) -> None:
    """Build a PlanApplyService with a working (mocked) logger."""
    plan = _make_mock_plan(error_details={"pre_existing": "value"})
    context.branch_plan = plan

    errored_plan = _make_mock_plan(
        state=ProcessingState.ERRORED,
        error_details={"pre_existing": "value", "merge_conflict": "test"},
    )
    lifecycle = _make_lifecycle_mock(plan)
    lifecycle.fail_apply.return_value = errored_plan
    context.branch_lifecycle = lifecycle

    service = PlanApplyService(
        lifecycle_service=lifecycle,
        changeset_store=None,
    )

    # Replace logger with a mock that does NOT raise
    mock_logger = MagicMock()
    service._logger = mock_logger

    context.branch_service = service
    context.branch_mock_logger = mock_logger


@when("pas_branch I call handle_merge_failure normally")
def step_call_handle_merge_failure_normal(context: Context) -> None:
    """Call handle_merge_failure expecting it to succeed."""
    context.branch_returned_plan = context.branch_service.handle_merge_failure(
        plan_id=_PLAN_ID,
        conflict_details="minor conflict in utils.py",
    )


@then("pas_branch the returned plan should not be None")
def step_returned_plan_not_none(context: Context) -> None:
    """Assert the method returned a plan (did not raise)."""
    assert context.branch_returned_plan is not None, (
        "Expected handle_merge_failure to return a plan, got None"
    )


@then(
    "pas_branch the service logger.error should have been called with merge failure details"
)
def step_logger_error_called(context: Context) -> None:
    """Assert logger.error was called with the expected arguments."""
    context.branch_mock_logger.error.assert_called_once_with(
        "Merge failure handled",
        plan_id=_PLAN_ID,
        conflict_details="minor conflict in utils.py",
    )
