"""Steps for execute_error_recovery.feature."""

from __future__ import annotations

from unittest.mock import MagicMock, patch

from behave import given, then, when
from typer.testing import CliRunner

from cleveragents.cli.commands.plan import app as plan_app
from cleveragents.domain.models.core.plan import PlanPhase, ProcessingState

_PLAN_ID = "01HXM8C2ZK4Q7C2B3F2R4VYV6J"


@given('a mocked plan in execute/errored with exception_type "{exc_type}" for eer')
def step_mock_errored_plan(context: object, exc_type: str) -> None:
    plan = MagicMock()
    plan.phase = PlanPhase.EXECUTE
    plan.state = ProcessingState.ERRORED
    plan.processing_state = ProcessingState.ERRORED
    plan.error_details = {"exception_type": exc_type}
    plan.identity.plan_id = _PLAN_ID
    plan.read_only = False
    plan.project_links = []
    plan.automation_profile = None
    plan.timestamps = MagicMock()

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

    def fake_execute_plan(pid: str) -> MagicMock:
        plan.phase = PlanPhase.EXECUTE
        plan.state = ProcessingState.QUEUED
        plan.processing_state = ProcessingState.QUEUED
        return plan

    mock_service.execute_plan.side_effect = fake_execute_plan

    def fake_revert_plan(
        pid: str,
        to_phase: object = None,
        reason: str = "",
    ) -> MagicMock:
        plan.phase = PlanPhase.STRATEGIZE
        plan.state = ProcessingState.QUEUED
        plan.processing_state = ProcessingState.QUEUED
        return plan

    mock_service.revert_plan.side_effect = fake_revert_plan

    context.eer_committed_plans = []

    def fake_commit(p: object) -> None:
        p.state = p.processing_state
        context.eer_committed_plans.append(
            {
                "phase": p.phase,
                "processing_state": p.processing_state,
                "error_details": (dict(p.error_details) if p.error_details else None),
            }
        )

    mock_service.commit_plan.side_effect = fake_commit

    mock_executor = MagicMock()

    def fake_run_strategize(pid: str) -> None:
        plan.phase = PlanPhase.STRATEGIZE
        plan.state = ProcessingState.COMPLETE
        plan.processing_state = ProcessingState.COMPLETE

    mock_executor.run_strategize.side_effect = fake_run_strategize

    def fake_run_execute(pid: str) -> None:
        plan.phase = PlanPhase.EXECUTE
        plan.state = ProcessingState.COMPLETE
        plan.processing_state = ProcessingState.COMPLETE
        plan.error_details = None

    mock_executor.run_execute.side_effect = fake_run_execute

    context.eer_runner = CliRunner()
    context.eer_service = mock_service
    context.eer_executor = mock_executor
    context.eer_plan = plan
    context.eer_exc_type = exc_type


@given("a mocked plan in execute/errored with no exception_type for eer")
def step_mock_errored_no_type(context: object) -> None:
    # Reuse the main step with empty string
    step_mock_errored_plan(context, "")


@given("revert_plan raises PlanError for max reversions for eer")
def step_revert_raises_max(context: object) -> None:
    from cleveragents.core.exceptions import PlanError

    context.eer_service.revert_plan.side_effect = PlanError(
        "Plan has exceeded the maximum number of reversions (3). "
        "Manual intervention required."
    )


@given("the strategy revision will fail for eer")
def step_strategy_will_fail(context: object) -> None:
    plan = context.eer_plan

    def fake_fail(pid: str) -> None:
        plan.phase = PlanPhase.STRATEGIZE
        plan.state = ProcessingState.ERRORED
        plan.processing_state = ProcessingState.ERRORED

    context.eer_executor.run_strategize.side_effect = fake_fail


@given("the plan disappears after strategize for eer")
def step_plan_disappears(context: object) -> None:
    plan = context.eer_plan
    call_count = {"n": 0}

    def fake_ok(pid: str) -> None:
        plan.phase = PlanPhase.STRATEGIZE
        plan.state = ProcessingState.COMPLETE
        plan.processing_state = ProcessingState.COMPLETE

    context.eer_executor.run_strategize.side_effect = fake_ok

    def get_plan_then_none(pid: str) -> object:
        call_count["n"] += 1
        # Calls: 1=errored check, 2=after commit, 3=after revert,
        # 4=after strategize → None
        if call_count["n"] >= 4:
            return None
        return plan

    context.eer_service.get_plan.side_effect = get_plan_then_none


@given("the plan disappears after transient recovery for eer")
def step_plan_disappears_transient(context: object) -> None:
    plan = context.eer_plan
    call_count = {"n": 0}

    def get_plan_then_none(pid: str) -> object:
        call_count["n"] += 1
        # Calls: 1=caller phase detection, 2=errored check,
        # 3=after commit → None
        if call_count["n"] >= 3:
            return None
        return plan

    context.eer_service.get_plan.side_effect = get_plan_then_none


@given('the mock error_details include sensitive data "{key}" for eer')
def step_add_sensitive_data(context: object, key: str) -> None:
    context.eer_plan.error_details[key] = "SECRET_VALUE_12345"


@given("the mock error_details include strategy_decisions_json for eer")
def step_add_strategy_json(context: object) -> None:
    context.eer_plan.error_details["strategy_decisions_json"] = '{"root": "decision1"}'


@given("the auto_progress moves the plan to Execute after strategize for eer")
def step_auto_progress(context: object) -> None:
    plan = context.eer_plan

    def fake_auto(pid: str) -> None:
        plan.phase = PlanPhase.EXECUTE
        plan.state = ProcessingState.QUEUED
        plan.processing_state = ProcessingState.QUEUED

    context.eer_executor.run_strategize.side_effect = fake_auto


@when("I invoke plan execute on the errored plan for eer")
def step_invoke_execute(context: object) -> None:
    with (
        patch(
            "cleveragents.cli.commands.plan._get_lifecycle_service",
            return_value=context.eer_service,
        ),
        patch(
            "cleveragents.cli.commands.plan._create_sandbox_for_plan",
            return_value=(None, []),
        ),
        patch(
            "cleveragents.cli.commands.plan._get_plan_executor",
            return_value=context.eer_executor,
        ),
        patch("cleveragents.cli.commands.plan._validate_plan_ulid"),
    ):
        context.eer_result = context.eer_runner.invoke(
            plan_app,
            ["execute", _PLAN_ID],
        )
    context.eer_output = context.eer_result.output


@then("the plan should have been reset to execute/queued for eer")
def step_check_reset(context: object) -> None:
    committed = context.eer_committed_plans
    assert len(committed) >= 1, "Expected at least one commit_plan call"
    first = committed[0]
    assert first["phase"] == PlanPhase.EXECUTE
    assert first["processing_state"] == ProcessingState.QUEUED


@then("run_execute should have been called for eer")
def step_check_run_execute(context: object) -> None:
    context.eer_executor.run_execute.assert_called_with(_PLAN_ID)


@then("run_strategize should have been called for eer")
def step_check_run_strategize(context: object) -> None:
    context.eer_executor.run_strategize.assert_called_with(_PLAN_ID)


@then("the plan should have been reverted to strategize for eer")
def step_check_reverted(context: object) -> None:
    context.eer_service.revert_plan.assert_called_with(
        _PLAN_ID,
        PlanPhase.STRATEGIZE,
        reason="execute_error_recovery",
    )


@then('the CLI output should contain "{text}" for eer')
def step_check_output(context: object, text: str) -> None:
    assert text in context.eer_output, (
        f"Expected '{text}' in output:\n{context.eer_output[:500]}"
    )


@then("the committed plan should have reversion_reason in error_details for eer")
def step_check_error_details(context: object) -> None:
    committed = context.eer_committed_plans
    assert len(committed) >= 1
    details = committed[0]["error_details"]
    assert details is not None
    assert "reversion_reason" in details
    assert details["reversion_reason"] == "execute_error_recovery"


@then("the CLI exit code should be non-zero for eer")
def step_check_exit_code(context: object) -> None:
    assert context.eer_result.exit_code != 0, (
        f"Expected non-zero but got {context.eer_result.exit_code}"
    )


@then("the CLI exit code should be zero for eer")
def step_check_exit_code_zero(context: object) -> None:
    assert context.eer_result.exit_code == 0, (
        f"Expected zero but got {context.eer_result.exit_code}\n"
        f"Output: {context.eer_output[:300]}"
    )


@then("the committed error_details should contain strategy_decisions_json for eer")
def step_check_strategy_json(context: object) -> None:
    committed = context.eer_committed_plans
    assert len(committed) >= 1
    details = committed[0]["error_details"]
    assert details is not None
    assert "strategy_decisions_json" in details


@then('the committed prior_error_details should not contain "{text}" for eer')
def step_check_redacted(context: object, text: str) -> None:
    committed = context.eer_committed_plans
    assert len(committed) >= 1
    details = committed[0]["error_details"]
    assert details is not None
    prior = details.get("prior_error_details", "")
    assert text not in prior
