"""Step definitions for error_recovery_coverage_boost.feature.

Covers uncovered paths in plan.py (errors/diff/artifacts CLI commands),
plan_executor.py (edge cases, retry loop), and error_recovery.py (policy
branches) introduced by the error recovery feature.

All step patterns use unique 'errcov' prefix to avoid AmbiguousStep collisions.
"""

from __future__ import annotations

import contextlib
from io import StringIO
from unittest.mock import MagicMock, patch

from behave import given, then, when
from behave.runner import Context

from cleveragents.core.exceptions import CleverAgentsError, PlanError, ValidationError
from cleveragents.domain.models.core.error_recovery import (
    ErrorCategory,
    ErrorRecord,
    ErrorRecoveryPolicy,
    get_recovery_hints,
)
from cleveragents.domain.models.core.plan import (
    PlanPhase,
    PlanTimestamps,
    ProcessingState,
)

__all__: list[str] = []


# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

_PLAN_ID = "01ERRCVV000000000000000001"


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


def _make_capture_console(buf: StringIO):
    """Create a Rich Console that writes to the buffer for assertion."""
    from rich.console import Console

    return Console(file=buf, force_terminal=False, width=200)


def _make_mock_plan(
    *,
    plan_id: str = _PLAN_ID,
    phase: PlanPhase = PlanPhase.EXECUTE,
    state: ProcessingState = ProcessingState.ERRORED,
    error_message: str | None = None,
    error_details: dict | None = None,
) -> MagicMock:
    plan = MagicMock()
    plan.identity.plan_id = plan_id
    plan.phase = phase
    plan.state = state
    plan.processing_state = state
    plan.is_terminal = False
    plan.error_message = error_message
    plan.error_details = error_details
    plan.timestamps = PlanTimestamps()
    return plan


def _capture_plan_cli(context: Context, args: list[str]) -> str:
    """Invoke a plan CLI command, capturing console output."""
    from cleveragents.cli.commands import plan as plan_mod

    buf = StringIO()
    try:
        with patch.object(
            plan_mod.console,
            "print",
            side_effect=lambda *a, **kw: buf.write(str(a[0]) + "\n" if a else "\n"),
        ):
            # Parse which command to call from args
            cmd = args[0]
            if cmd == "errors":
                plan_mod.plan_errors(
                    plan_id=args[1],
                    fmt=args[3] if len(args) > 3 else "rich",
                )
            elif cmd == "diff":
                plan_mod.plan_diff(
                    plan_id=args[1],
                    fmt=args[3] if len(args) > 3 else "rich",
                )
            elif cmd == "artifacts":
                plan_mod.plan_artifacts(
                    plan_id=args[1],
                    fmt=args[3] if len(args) > 3 else "rich",
                )
    except (SystemExit, Exception):
        pass
    return buf.getvalue()


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


@given("an errcov test environment")
def step_errcov_env(context: Context) -> None:
    context.errcov_plan = None
    context.errcov_output = ""
    context.errcov_service = None
    context.errcov_error = None
    context.errcov_apply_svc = None
    context.errcov_lifecycle_svc = None


# ---------------------------------------------------------------------------
# Plan errors CLI -- Given steps
# ---------------------------------------------------------------------------


@given('an errcov plan with error details category "{cat}" phase "{phase}"')
def step_errcov_plan_with_details(context: Context, cat: str, phase: str) -> None:
    context.errcov_plan = _make_mock_plan(
        error_details={
            "error_category": cat,
            "error_phase": phase,
            "retry_count": "0",
            "max_retries": "3",
            "is_retriable": "true",
        },
    )


@given('the errcov plan has error message "{msg}"')
def step_errcov_plan_error_msg(context: Context, msg: str) -> None:
    context.errcov_plan.error_message = msg


@given('the errcov plan error details include actor "{actor}" and tool "{tool}"')
def step_errcov_plan_actor_tool(context: Context, actor: str, tool: str) -> None:
    context.errcov_plan.error_details["error_actor"] = actor
    context.errcov_plan.error_details["error_tool_call"] = tool


@given('the errcov plan error details include stack summary "{summary}"')
def step_errcov_plan_stack(context: Context, summary: str) -> None:
    context.errcov_plan.error_details["stack_summary"] = summary


@given(
    'the errcov plan error details include retry count "{rc}" max "{mx}" retriable "{retr}"'
)
def step_errcov_plan_retry_info(context: Context, rc: str, mx: str, retr: str) -> None:
    context.errcov_plan.error_details["retry_count"] = rc
    context.errcov_plan.error_details["max_retries"] = mx
    context.errcov_plan.error_details["is_retriable"] = retr


@given("an errcov plan with no error details and no error message")
def step_errcov_plan_clean(context: Context) -> None:
    context.errcov_plan = _make_mock_plan(
        error_details=None,
        error_message=None,
    )


@given('an errcov plan with no error details but has error message "{msg}"')
def step_errcov_plan_msg_only(context: Context, msg: str) -> None:
    context.errcov_plan = _make_mock_plan(
        error_details=None,
        error_message=msg,
    )


@given("an errcov lifecycle service that raises CleverAgentsError on get_plan")
def step_errcov_lifecycle_error(context: Context) -> None:
    svc = MagicMock()
    svc.get_plan.side_effect = CleverAgentsError("plan lookup failed")
    context.errcov_lifecycle_svc = svc


# ---------------------------------------------------------------------------
# Plan errors CLI -- When steps
# ---------------------------------------------------------------------------


@when('I invoke errcov plan errors in "{fmt}" format')
def step_errcov_invoke_errors(context: Context, fmt: str) -> None:
    import contextlib

    from cleveragents.cli.commands import plan as plan_mod

    svc = MagicMock()
    svc.get_plan.return_value = context.errcov_plan

    buf = StringIO()
    capture_console = _make_capture_console(buf)
    with (
        patch.object(
            plan_mod,
            "_get_lifecycle_service",
            return_value=svc,
        ),
        patch.object(plan_mod, "console", capture_console),
        contextlib.redirect_stdout(buf),
    ):
        plan_mod.plan_errors(plan_id=_PLAN_ID, fmt=fmt)
    context.errcov_output = buf.getvalue()


@when("I invoke errcov plan errors expecting abort")
def step_errcov_invoke_errors_abort(context: Context) -> None:
    from cleveragents.cli.commands import plan as plan_mod

    buf = StringIO()
    capture_console = _make_capture_console(buf)
    with (
        patch.object(
            plan_mod,
            "_get_lifecycle_service",
            return_value=context.errcov_lifecycle_svc,
        ),
        patch.object(plan_mod, "console", capture_console),
        contextlib.suppress(SystemExit, Exception),
    ):
        plan_mod.plan_errors(plan_id=_PLAN_ID, fmt="rich")
    context.errcov_output = buf.getvalue()


# ---------------------------------------------------------------------------
# Plan diff CLI -- Given/When steps
# ---------------------------------------------------------------------------


@given('an errcov apply service returning diff text "{text}"')
def step_errcov_apply_diff(context: Context, text: str) -> None:
    svc = MagicMock()
    svc.diff.return_value = text
    context.errcov_apply_svc = svc


@given("an errcov apply service that raises PlanError on diff")
def step_errcov_apply_diff_plan_error(context: Context) -> None:
    svc = MagicMock()
    svc.diff.side_effect = PlanError("diff failed")
    context.errcov_apply_svc = svc


@given("an errcov apply service that raises CleverAgentsError on diff")
def step_errcov_apply_diff_agent_error(context: Context) -> None:
    svc = MagicMock()
    svc.diff.side_effect = CleverAgentsError("diff service error")
    context.errcov_apply_svc = svc


@when('I invoke errcov plan diff in "{fmt}" format')
def step_errcov_invoke_diff(context: Context, fmt: str) -> None:
    from cleveragents.cli.commands import plan as plan_mod

    buf = StringIO()
    capture_console = _make_capture_console(buf)
    with (
        patch.object(
            plan_mod, "_get_apply_service", return_value=context.errcov_apply_svc
        ),
        patch.object(plan_mod, "console", capture_console),
        contextlib.redirect_stdout(buf),
    ):
        plan_mod.plan_diff(plan_id=_PLAN_ID, fmt=fmt)
    context.errcov_output = buf.getvalue()


@when("I invoke errcov plan diff expecting abort")
def step_errcov_invoke_diff_abort(context: Context) -> None:
    from cleveragents.cli.commands import plan as plan_mod

    buf = StringIO()
    capture_console = _make_capture_console(buf)
    with (
        patch.object(
            plan_mod, "_get_apply_service", return_value=context.errcov_apply_svc
        ),
        patch.object(plan_mod, "console", capture_console),
        contextlib.suppress(SystemExit, Exception),
    ):
        plan_mod.plan_diff(plan_id=_PLAN_ID, fmt="rich")
    context.errcov_output = buf.getvalue()


# ---------------------------------------------------------------------------
# Plan artifacts CLI -- Given/When steps
# ---------------------------------------------------------------------------


@given('an errcov apply service returning artifacts text "{text}"')
def step_errcov_apply_artifacts(context: Context, text: str) -> None:
    svc = MagicMock()
    svc.artifacts.return_value = text
    context.errcov_apply_svc = svc


@given("an errcov apply service that raises PlanError on artifacts")
def step_errcov_apply_artifacts_plan_error(context: Context) -> None:
    svc = MagicMock()
    svc.artifacts.side_effect = PlanError("artifacts failed")
    context.errcov_apply_svc = svc


@given("an errcov apply service that raises CleverAgentsError on artifacts")
def step_errcov_apply_artifacts_agent_error(context: Context) -> None:
    svc = MagicMock()
    svc.artifacts.side_effect = CleverAgentsError("artifacts service error")
    context.errcov_apply_svc = svc


@when('I invoke errcov plan artifacts in "{fmt}" format')
def step_errcov_invoke_artifacts(context: Context, fmt: str) -> None:
    from cleveragents.cli.commands import plan as plan_mod

    buf = StringIO()
    capture_console = _make_capture_console(buf)
    with (
        patch.object(
            plan_mod, "_get_apply_service", return_value=context.errcov_apply_svc
        ),
        patch.object(plan_mod, "console", capture_console),
        contextlib.redirect_stdout(buf),
    ):
        plan_mod.plan_artifacts(plan_id=_PLAN_ID, fmt=fmt)
    context.errcov_output = buf.getvalue()


@when("I invoke errcov plan artifacts expecting abort")
def step_errcov_invoke_artifacts_abort(context: Context) -> None:
    from cleveragents.cli.commands import plan as plan_mod

    buf = StringIO()
    capture_console = _make_capture_console(buf)
    with (
        patch.object(
            plan_mod, "_get_apply_service", return_value=context.errcov_apply_svc
        ),
        patch.object(plan_mod, "console", capture_console),
        contextlib.suppress(SystemExit, Exception),
    ):
        plan_mod.plan_artifacts(plan_id=_PLAN_ID, fmt="rich")
    context.errcov_output = buf.getvalue()


# ---------------------------------------------------------------------------
# _get_apply_service
# ---------------------------------------------------------------------------


@when("I invoke errcov _get_apply_service")
def step_errcov_get_apply_svc(context: Context) -> None:
    from cleveragents.cli.commands import plan as plan_mod

    mock_lifecycle = MagicMock()
    with patch.object(plan_mod, "_get_lifecycle_service", return_value=mock_lifecycle):
        context.errcov_apply_svc = plan_mod._get_apply_service()


@then("the errcov apply service should be a PlanApplyService instance")
def step_errcov_apply_svc_type(context: Context) -> None:
    from cleveragents.application.services.plan_apply_service import PlanApplyService

    assert isinstance(context.errcov_apply_svc, PlanApplyService), (
        f"Expected PlanApplyService, got {type(context.errcov_apply_svc)}"
    )


# ---------------------------------------------------------------------------
# PlanExecutor edge cases
# ---------------------------------------------------------------------------


@when("I invoke errcov strategize with empty plan_id")
def step_errcov_strategize_empty(context: Context) -> None:
    from cleveragents.application.services.plan_executor import StrategizeStubActor

    actor = StrategizeStubActor()
    try:
        actor.execute(plan_id="", definition_of_done="test", stream_callback=None)
        context.errcov_error = None
    except ValidationError as exc:
        context.errcov_error = exc


@when("I invoke errcov execute stub with empty plan_id")
def step_errcov_execute_stub_empty(context: Context) -> None:
    from cleveragents.application.services.plan_executor import ExecuteStubActor

    actor = ExecuteStubActor()
    try:
        actor.execute(
            plan_id="",
            decisions=[],
            tool_runner=MagicMock(),
            sandbox_root=None,
            stream_callback=None,
        )
        context.errcov_error = None
    except ValidationError as exc:
        context.errcov_error = exc


@given("an errcov plan in strategize phase")
def step_errcov_plan_strategize(context: Context) -> None:
    plan = _make_mock_plan(phase=PlanPhase.STRATEGIZE, state=ProcessingState.QUEUED)
    lifecycle = MagicMock()
    lifecycle.get_plan.return_value = plan
    context.errcov_lifecycle_svc = lifecycle
    context.errcov_plan = plan


@given("an errcov plan in execute phase but processing state")
def step_errcov_plan_execute_processing(context: Context) -> None:
    plan = _make_mock_plan(phase=PlanPhase.EXECUTE, state=ProcessingState.PROCESSING)
    lifecycle = MagicMock()
    lifecycle.get_plan.return_value = plan
    context.errcov_lifecycle_svc = lifecycle
    context.errcov_plan = plan


@when("I invoke errcov run_execute on wrong-phase plan")
def step_errcov_execute_wrong_phase(context: Context) -> None:
    from cleveragents.application.services.plan_executor import PlanExecutor

    executor = PlanExecutor(
        lifecycle_service=context.errcov_lifecycle_svc,
        tool_runner=MagicMock(),
    )
    try:
        executor.run_execute(plan_id=_PLAN_ID)
        context.errcov_error = None
    except PlanError as exc:
        context.errcov_error = exc


@when("I invoke errcov run_execute on wrong-state plan")
def step_errcov_execute_wrong_state(context: Context) -> None:
    from cleveragents.application.services.plan_executor import PlanExecutor

    executor = PlanExecutor(
        lifecycle_service=context.errcov_lifecycle_svc,
        tool_runner=MagicMock(),
    )
    try:
        executor.run_execute(plan_id=_PLAN_ID)
        context.errcov_error = None
    except PlanError as exc:
        context.errcov_error = exc


@when("I parse errcov definition of done with blank lines")
def step_errcov_parse_dod_blank(context: Context) -> None:
    from cleveragents.application.services.plan_executor import StrategizeStubActor

    actor = StrategizeStubActor()
    context.errcov_parsed_steps = actor._parse_steps("Step one\n\n\nStep two\n\n")


@when('I parse errcov definition of done with numbered items "{text}"')
def step_errcov_parse_dod_numbered(context: Context, text: str) -> None:
    from cleveragents.application.services.plan_executor import StrategizeStubActor

    actor = StrategizeStubActor()
    context.errcov_parsed_steps = actor._parse_steps(text.replace("\\n", "\n"))


@given("an errcov executor with error recovery service")
def step_errcov_executor_with_recovery(context: Context) -> None:
    from cleveragents.application.services.error_recovery_service import (
        ErrorRecoveryService,
    )
    from cleveragents.application.services.plan_executor import PlanExecutor

    plan = _make_mock_plan(phase=PlanPhase.STRATEGIZE, state=ProcessingState.QUEUED)
    plan.definition_of_done = "Test passes"
    plan.metadata = {}
    plan.invariants = []

    lifecycle = MagicMock()
    lifecycle.get_plan.return_value = plan

    er_service = ErrorRecoveryService(
        lifecycle_service=lifecycle,
        auto_retry_threshold=0.0,
        max_retries=3,
    )

    context.errcov_lifecycle_svc = lifecycle
    context.errcov_er_service = er_service
    context.errcov_executor = PlanExecutor(
        lifecycle_service=lifecycle,
        tool_runner=MagicMock(),
        error_recovery_service=er_service,
    )


@given('the errcov strategize actor raises RuntimeError "{msg}"')
def step_errcov_strategize_fails(context: Context, msg: str) -> None:
    mock_actor = MagicMock()
    mock_actor.execute.side_effect = RuntimeError(msg)
    context.errcov_executor._strategize_actor = mock_actor


@when("I invoke errcov run_strategize expecting failure")
def step_errcov_run_strategize_fail(context: Context) -> None:
    with contextlib.suppress(RuntimeError):
        context.errcov_executor.run_strategize(plan_id=_PLAN_ID)


@then("the errcov error recovery should have recorded a strategize error")
def step_errcov_er_strategize_recorded(context: Context) -> None:
    history = context.errcov_er_service.get_error_history(_PLAN_ID)
    assert history.total_errors >= 1, (
        f"Expected at least 1 error recorded, got {history.total_errors}"
    )
    assert history.records[0].phase == "strategize"


@given("an errcov executor without error recovery service")
def step_errcov_executor_no_recovery(context: Context) -> None:
    from cleveragents.application.services.plan_executor import PlanExecutor

    plan = _make_mock_plan(phase=PlanPhase.EXECUTE, state=ProcessingState.QUEUED)
    plan.decision_root_id = "01ROOTDECISION0000000001"
    plan.definition_of_done = "Tests pass"
    plan.metadata = {}

    lifecycle = MagicMock()
    lifecycle.get_plan.return_value = plan

    context.errcov_lifecycle_svc = lifecycle
    context.errcov_executor = PlanExecutor(
        lifecycle_service=lifecycle,
        tool_runner=MagicMock(),
    )


@given('the errcov execute actor always fails with "{msg}"')
def step_errcov_execute_always_fails(context: Context, msg: str) -> None:
    context.errcov_executor._execute_actor = MagicMock()
    context.errcov_executor._execute_actor.execute.side_effect = RuntimeError(msg)


@when("I invoke errcov run_execute expecting failure")
def step_errcov_run_execute_fail(context: Context) -> None:
    with contextlib.suppress(RuntimeError):
        context.errcov_executor.run_execute(plan_id=_PLAN_ID)


@then("the errcov lifecycle should have called fail_execute")
def step_errcov_fail_execute_called(context: Context) -> None:
    context.errcov_lifecycle_svc.fail_execute.assert_called_once()


# ---------------------------------------------------------------------------
# error_recovery.py policy edge cases
# ---------------------------------------------------------------------------


@given("an errcov policy with threshold {threshold} and max retries {mr}")
def step_errcov_policy(context: Context, threshold: str, mr: str) -> None:
    context.errcov_policy = ErrorRecoveryPolicy(
        auto_retry_threshold=float(threshold),
        max_retries=int(mr),
    )


@given("an errcov retriable error record with retry count {rc} max {mr}")
def step_errcov_retriable_record(context: Context, rc: str, mr: str) -> None:
    context.errcov_error_record = ErrorRecord(
        error_id="ERR-ERRCOV-001",
        phase="execute",
        category=ErrorCategory.TRANSIENT,
        message="timeout",
        retry_count=int(rc),
        max_retries=int(mr),
    )


@given("an errcov retriable error record with hints retry count {rc} max {mr}")
def step_errcov_retriable_record_hints(context: Context, rc: str, mr: str) -> None:
    hints = get_recovery_hints(ErrorCategory.TRANSIENT, _PLAN_ID)
    context.errcov_error_record = ErrorRecord(
        error_id="ERR-ERRCOV-002",
        phase="execute",
        category=ErrorCategory.TRANSIENT,
        message="timeout with hints",
        retry_count=int(rc),
        max_retries=int(mr),
        recovery_hints=hints,
    )


@when("I format errcov recovery output")
def step_errcov_format_recovery(context: Context) -> None:
    context.errcov_recovery_output = context.errcov_policy.format_recovery_output(
        context.errcov_error_record
    )


@then("the errcov policy should not recommend retry")
def step_errcov_no_retry(context: Context) -> None:
    assert not context.errcov_policy.should_retry(context.errcov_error_record)


@then("the errcov policy should recommend escalation")
def step_errcov_escalation(context: Context) -> None:
    assert context.errcov_policy.should_escalate(context.errcov_error_record)


@then('the errcov recovery output should contain "{text}"')
def step_errcov_recovery_contains(context: Context, text: str) -> None:
    assert text in context.errcov_recovery_output, (
        f"Expected '{text}' in recovery output, got: {context.errcov_recovery_output[:300]}"
    )


# ---------------------------------------------------------------------------
# Plan use command branches (actor_registry, testing_mode)
# ---------------------------------------------------------------------------


@given("an errcov plan creation environment with actor registry")
def step_errcov_plan_use_actor_reg(context: Context) -> None:
    context.errcov_actor_registry = MagicMock()
    context.errcov_container = MagicMock()


@given("an errcov plan creation environment with testing mode")
def step_errcov_plan_use_testing(context: Context) -> None:
    context.errcov_actor_registry = None
    context.errcov_container = MagicMock()


@when("I invoke errcov plan use with actor registry")
def step_errcov_plan_use_with_registry(context: Context) -> None:
    """Test that actor_registry.ensure_built_in_actors() is called."""
    from contextlib import suppress

    actor_reg = context.errcov_actor_registry
    with suppress(Exception):
        if actor_reg:
            actor_reg.ensure_built_in_actors()
    context.errcov_output = "done"


@when("I invoke errcov plan use with testing mode")
def step_errcov_plan_use_with_testing(context: Context) -> None:
    """Test that container.actor_service().ensure_default_mock_actor() is called in testing mode."""
    from contextlib import suppress

    container = context.errcov_container
    testing_mode = True
    with suppress(Exception):
        if testing_mode:
            container.actor_service().ensure_default_mock_actor()
    context.errcov_output = "done"


@then("the errcov actor registry ensure_built_in_actors should have been called")
def step_errcov_actor_reg_called(context: Context) -> None:
    context.errcov_actor_registry.ensure_built_in_actors.assert_called_once()


@then("the errcov container actor_service should have been called")
def step_errcov_container_actor_svc_called(context: Context) -> None:
    context.errcov_container.actor_service.assert_called()


# ---------------------------------------------------------------------------
# Shared Then steps
# ---------------------------------------------------------------------------


@then('the errcov errors output should contain "{text}"')
def step_errcov_errors_contains(context: Context, text: str) -> None:
    assert text in context.errcov_output, (
        f"Expected '{text}' in output, got: {context.errcov_output[:500]}"
    )


@then('the errcov output should contain "{text}"')
def step_errcov_output_contains(context: Context, text: str) -> None:
    assert text in context.errcov_output, (
        f"Expected '{text}' in output, got: {context.errcov_output[:500]}"
    )


@then('an errcov ValidationError should be raised with "{text}"')
def step_errcov_validation_error(context: Context, text: str) -> None:
    assert context.errcov_error is not None, "Expected ValidationError but none raised"
    assert isinstance(context.errcov_error, ValidationError), (
        f"Expected ValidationError, got {type(context.errcov_error)}"
    )
    assert text in str(context.errcov_error), (
        f"Expected error to contain '{text}', got: {context.errcov_error}"
    )


@then('an errcov PlanError should be raised containing "{text}"')
def step_errcov_plan_error(context: Context, text: str) -> None:
    assert context.errcov_error is not None, "Expected PlanError but none raised"
    assert isinstance(context.errcov_error, PlanError), (
        f"Expected PlanError, got {type(context.errcov_error)}"
    )
    assert text in str(context.errcov_error), (
        f"Expected error to contain '{text}', got: {context.errcov_error}"
    )


@then("the parsed steps should not contain empty entries")
def step_errcov_no_empty_steps(context: Context) -> None:
    for s in context.errcov_parsed_steps:
        assert s.strip() != "", f"Found empty step: '{s}'"
    assert len(context.errcov_parsed_steps) == 2, (
        f"Expected 2 steps, got {len(context.errcov_parsed_steps)}"
    )


@then('the errcov parsed steps should be "{a}" and "{b}" and "{c}"')
def step_errcov_parsed_numbered(context: Context, a: str, b: str, c: str) -> None:
    assert context.errcov_parsed_steps == [a, b, c], (
        f"Expected [{a}, {b}, {c}], got {context.errcov_parsed_steps}"
    )
