"""Step definitions for plan_lifecycle_service_coverage_r3.feature.

Targets uncovered lines in PlanLifecycleService:
- Lines 318-323: _run_estimation exception handler
- Line 452:      _resolve_actor_registry_entry dunder prefix
- Lines 640-659: list_actions persistence paths (namespace, list_all, cache,
                 state filter, DatabaseError fallback)
- Line 917:      list_plans persisted cache refresh
- Lines 1592-1614: _complete_apply_if_queued branches
- Lines 1687-1741: try_auto_run full lifecycle auto-progression
"""

from __future__ import annotations

from unittest.mock import MagicMock, patch

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

from cleveragents.application.services.plan_lifecycle_service import (
    PlanLifecycleService,
)
from cleveragents.config.settings import Settings
from cleveragents.core.exceptions import DatabaseError
from cleveragents.domain.models.core.action import Action, ActionState
from cleveragents.domain.models.core.plan import (
    AutomationProfileProvenance,
    AutomationProfileRef,
    NamespacedName,
    Plan,
    PlanIdentity,
    PlanPhase,
    PlanTimestamps,
    ProcessingState,
    ProjectLink,
)

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


def _make_settings(**overrides) -> Settings:
    """Create a Settings instance, clearing singleton."""
    Settings._instance = None
    return Settings(**overrides)


def _create_action_r3(context: Context, name: str, **kwargs) -> Action:
    """Create an action with sensible defaults."""
    defaults = {
        "name": name,
        "description": f"Action for {name}",
        "definition_of_done": "Tests pass",
        "strategy_actor": "openai/gpt-4",
        "execution_actor": "openai/gpt-4",
    }
    defaults.update(kwargs)
    return context.plscov3_svc.create_action(**defaults)


def _advance_to_strat_complete_r3(context: Context) -> None:
    """Advance plan to Strategize/COMPLETE."""
    pid = context.plscov3_plan.identity.plan_id
    svc = context.plscov3_svc
    svc.start_strategize(pid)
    plan = svc.get_plan(pid)
    plan.processing_state = ProcessingState.COMPLETE
    context.plscov3_plan = plan


def _advance_to_exec_complete_r3(context: Context) -> None:
    """Advance plan from Strategize/COMPLETE to Execute/COMPLETE."""
    pid = context.plscov3_plan.identity.plan_id
    svc = context.plscov3_svc
    svc.execute_plan(pid)
    svc.start_execute(pid)
    plan = svc.get_plan(pid)
    plan.processing_state = ProcessingState.COMPLETE
    context.plscov3_plan = plan


def _advance_to_apply_queued_r3(context: Context) -> None:
    """Advance plan to Apply/QUEUED."""
    _advance_to_strat_complete_r3(context)
    _advance_to_exec_complete_r3(context)
    pid = context.plscov3_plan.identity.plan_id
    svc = context.plscov3_svc
    svc.apply_plan(pid)
    context.plscov3_plan = svc.get_plan(pid)


def _advance_to_apply_processing_r3(context: Context) -> None:
    """Advance plan to Apply/PROCESSING."""
    _advance_to_apply_queued_r3(context)
    pid = context.plscov3_plan.identity.plan_id
    svc = context.plscov3_svc
    svc.start_apply(pid)
    context.plscov3_plan = svc.get_plan(pid)


def _make_mock_action(name: str, state: ActionState = ActionState.AVAILABLE) -> Action:
    """Create a real Action domain object for test doubles."""
    return Action(
        namespaced_name=NamespacedName.parse(name),
        description=f"Test action {name}",
        definition_of_done="Done",
        strategy_actor="openai/gpt-4",
        execution_actor="openai/gpt-4",
        state=state,
    )


def _make_mock_plan(plan_id: str) -> Plan:
    """Create a real Plan domain object for test doubles."""
    return Plan(
        identity=PlanIdentity(plan_id=plan_id),
        namespaced_name=NamespacedName.parse("local/test-plan"),
        description="Test plan",
        action_name="local/test",
        phase=PlanPhase.STRATEGIZE,
        processing_state=ProcessingState.QUEUED,
        timestamps=PlanTimestamps(),
    )


def _setup_mock_uow():
    """Create a mock UnitOfWork returning a mock context."""
    uow = MagicMock()
    ctx_mock = MagicMock()
    uow.transaction.return_value.__enter__ = MagicMock(return_value=ctx_mock)
    uow.transaction.return_value.__exit__ = MagicMock(return_value=False)
    return uow, ctx_mock


# =================================================================
# Background
# =================================================================


@given("plscov3 a fresh service is ready")
def step_bg_fresh_service(context: Context) -> None:
    settings = _make_settings()
    context.plscov3_svc = PlanLifecycleService(settings=settings)
    context.plscov3_error = None
    context.plscov3_plan = None


# =================================================================
# Estimation actor failure (lines 318-323)
# =================================================================


@given('plscov3 an action with estimation actor exists named "{name}" actor "{actor}"')
def step_create_action_est(context: Context, name: str, actor: str) -> None:
    _create_action_r3(context, name, estimation_actor=actor)


@given('plscov3 the action "{name}" is used on project "{project}"')
def step_use_action(context: Context, name: str, project: str) -> None:
    plan = context.plscov3_svc.use_action(
        name, project_links=[ProjectLink(project_name=project)]
    )
    context.plscov3_plan = plan


@given("plscov3 the current plan is advanced to Strategize COMPLETE")
def step_advance_strat_complete(context: Context) -> None:
    _advance_to_strat_complete_r3(context)


@when("plscov3 execute_plan is invoked with estimation stub raising")
def step_execute_estimation_raises(context: Context) -> None:
    pid = context.plscov3_plan.identity.plan_id
    svc = context.plscov3_svc

    # Patch the EstimationStubActor import inside _run_estimation
    patcher = patch(
        "cleveragents.application.services.plan_executor.EstimationStubActor"
    )
    mock_cls = patcher.start()
    context.add_cleanup(patcher.stop)
    mock_instance = MagicMock()
    mock_instance.estimate.side_effect = RuntimeError("estimation boom")
    mock_cls.return_value = mock_instance

    # Ensure plan has estimation_actor set so _run_estimation runs
    plan = svc.get_plan(pid)
    plan.estimation_actor = "est-actor"

    try:
        svc.execute_plan(pid)
    except Exception as exc:
        context.plscov3_error = exc


@then("plscov3 the current plan should be in Execute phase")
def step_assert_execute_phase(context: Context) -> None:
    pid = context.plscov3_plan.identity.plan_id
    plan = context.plscov3_svc.get_plan(pid)
    assert plan.phase == PlanPhase.EXECUTE, f"Expected Execute, got {plan.phase}"


# =================================================================
# _resolve_actor_registry_entry dunder prefix (line 452)
# =================================================================


@when('plscov3 resolve_actor_registry_entry is invoked for "{actor_name}"')
def step_resolve_dunder(context: Context, actor_name: str) -> None:
    context.plscov3_resolved = context.plscov3_svc._resolve_actor_registry_entry(
        actor_name
    )


@then("plscov3 the resolved actor entry is None")
def step_assert_resolved_none(context: Context) -> None:
    assert context.plscov3_resolved is None


# =================================================================
# list_actions persistence: namespace filter (lines 640-645)
# =================================================================


@given("plscov3 a service with mock UoW is ready")
def step_svc_mock_uow(context: Context) -> None:
    settings = _make_settings()
    uow, ctx_mock = _setup_mock_uow()
    ctx_mock.actions.get_by_namespace.return_value = []
    ctx_mock.actions.list_all.return_value = []
    context.plscov3_svc = PlanLifecycleService(settings=settings, unit_of_work=uow)
    context.plscov3_ctx_mock = ctx_mock
    context.plscov3_error = None


@when('plscov3 list_actions is invoked with namespace "{ns}" and state "{state}"')
def step_list_actions_ns_state(context: Context, ns: str, state: str) -> None:
    state_enum = ActionState(state)
    context.plscov3_actions_result = context.plscov3_svc.list_actions(
        namespace=ns, state=state_enum
    )


@then('plscov3 the mock get_by_namespace was invoked with "{ns}"')
def step_assert_get_by_namespace(context: Context, ns: str) -> None:
    context.plscov3_ctx_mock.actions.get_by_namespace.assert_called_once()
    args = context.plscov3_ctx_mock.actions.get_by_namespace.call_args
    assert args[0][0] == ns, f"Expected namespace '{ns}', got {args}"


# =================================================================
# list_actions persistence: no namespace (line 648)
# =================================================================


@when("plscov3 list_actions is invoked without any namespace")
def step_list_actions_no_ns(context: Context) -> None:
    context.plscov3_actions_result = context.plscov3_svc.list_actions()


@then("plscov3 the mock actions list_all was invoked")
def step_assert_actions_list_all(context: Context) -> None:
    context.plscov3_ctx_mock.actions.list_all.assert_called_once()


# =================================================================
# list_actions persistence: cache refresh (lines 650-653)
# =================================================================


@given("plscov3 a service with mock UoW returning DB actions is ready")
def step_svc_uow_db_actions(context: Context) -> None:
    settings = _make_settings()
    uow, ctx_mock = _setup_mock_uow()
    db_action = _make_mock_action("local/db-action")
    ctx_mock.actions.get_by_namespace.return_value = [db_action]
    context.plscov3_svc = PlanLifecycleService(settings=settings, unit_of_work=uow)
    context.plscov3_ctx_mock = ctx_mock
    context.plscov3_db_action = db_action
    context.plscov3_error = None


@when('plscov3 list_actions is invoked filtering by namespace "{ns}"')
def step_list_actions_filter_ns(context: Context, ns: str) -> None:
    context.plscov3_actions_result = context.plscov3_svc.list_actions(namespace=ns)


@then("plscov3 the actions in-memory cache has the DB action")
def step_assert_cache_has_action(context: Context) -> None:
    key = str(context.plscov3_db_action.namespaced_name)
    assert key in context.plscov3_svc._actions, (
        f"Expected '{key}' in cache, got {list(context.plscov3_svc._actions.keys())}"
    )


# =================================================================
# list_actions persistence: state filter on list_all (lines 655-656)
# =================================================================


@given("plscov3 a service with mock UoW returning mixed-state actions is ready")
def step_svc_uow_mixed_actions(context: Context) -> None:
    settings = _make_settings()
    uow, ctx_mock = _setup_mock_uow()
    avail = _make_mock_action("local/avail-action", ActionState.AVAILABLE)
    archived = _make_mock_action("local/archived-action", ActionState.ARCHIVED)
    ctx_mock.actions.list_all.return_value = [avail, archived]
    context.plscov3_svc = PlanLifecycleService(settings=settings, unit_of_work=uow)
    context.plscov3_ctx_mock = ctx_mock
    context.plscov3_error = None


@when('plscov3 list_actions is invoked without namespace but with state "{state}"')
def step_list_actions_state_only(context: Context, state: str) -> None:
    state_enum = ActionState(state)
    context.plscov3_actions_result = context.plscov3_svc.list_actions(state=state_enum)


@then("plscov3 only actions in available state are returned")
def step_assert_only_available(context: Context) -> None:
    result = context.plscov3_actions_result
    assert len(result) == 1, f"Expected 1 action, got {len(result)}"
    assert result[0].state == ActionState.AVAILABLE


# =================================================================
# list_actions persistence: DatabaseError fallback (lines 658-659)
# =================================================================


@given("plscov3 a service with failing DB actions is ready")
def step_svc_failing_db(context: Context) -> None:
    settings = _make_settings()
    uow, ctx_mock = _setup_mock_uow()
    ctx_mock.actions.list_all.side_effect = DatabaseError("DB down")
    ctx_mock.actions.get_by_namespace.side_effect = DatabaseError("DB down")
    context.plscov3_svc = PlanLifecycleService(settings=settings, unit_of_work=uow)
    context.plscov3_error = None


@given('plscov3 an in-memory action "{name}" is added')
def step_add_inmem_action(context: Context, name: str) -> None:
    action = _make_mock_action(name)
    context.plscov3_svc._actions[str(action.namespaced_name)] = action
    context.plscov3_inmem_action = action


@when("plscov3 list_actions is invoked expecting DB fallback")
def step_list_actions_fallback(context: Context) -> None:
    context.plscov3_actions_result = context.plscov3_svc.list_actions()


@then("plscov3 the fallback in-memory action is returned")
def step_assert_fallback_action(context: Context) -> None:
    result = context.plscov3_actions_result
    assert len(result) >= 1, f"Expected >= 1 actions, got {len(result)}"
    names = [str(a.namespaced_name) for a in result]
    expected = str(context.plscov3_inmem_action.namespaced_name)
    assert expected in names, f"Expected '{expected}' in {names}"


# =================================================================
# list_plans persisted cache refresh (line 917)
# =================================================================


@given("plscov3 a service with mock UoW returning DB plans is ready")
def step_svc_uow_db_plans(context: Context) -> None:
    settings = _make_settings()
    uow, ctx_mock = _setup_mock_uow()

    from ulid import ULID

    plan_id = str(ULID())
    db_plan = _make_mock_plan(plan_id)
    ctx_mock.lifecycle_plans.list_all.return_value = [db_plan]

    context.plscov3_svc = PlanLifecycleService(settings=settings, unit_of_work=uow)
    context.plscov3_db_plan = db_plan
    context.plscov3_error = None


@when("plscov3 list_plans is invoked")
def step_list_plans(context: Context) -> None:
    context.plscov3_plans_result = context.plscov3_svc.list_plans()


@then("plscov3 the plans in-memory cache was refreshed")
def step_assert_plan_cache(context: Context) -> None:
    pid = context.plscov3_db_plan.identity.plan_id
    assert pid in context.plscov3_svc._plans, f"Expected '{pid}' in plan cache"


# =================================================================
# _complete_apply_if_queued - not in Apply phase (line 1592)
# =================================================================


@given('plscov3 a basic action named "{name}" is created')
def step_create_basic_action(context: Context, name: str) -> None:
    _create_action_r3(context, name)


@when("plscov3 complete_apply_if_queued is invoked")
def step_call_caiq(context: Context) -> None:
    pid = context.plscov3_plan.identity.plan_id
    try:
        result = context.plscov3_svc._complete_apply_if_queued(pid)
        context.plscov3_plan = result
    except Exception as exc:
        context.plscov3_error = exc


@then("plscov3 the current plan phase remains Strategize")
def step_assert_phase_strategize(context: Context) -> None:
    assert context.plscov3_plan.phase == PlanPhase.STRATEGIZE, (
        f"Expected Strategize, got {context.plscov3_plan.phase}"
    )


# =================================================================
# _complete_apply_if_queued - not QUEUED (line 1594)
# =================================================================


@given("plscov3 the current plan is advanced to Apply PROCESSING")
def step_advance_apply_processing(context: Context) -> None:
    _advance_to_apply_processing_r3(context)


@then("plscov3 the current plan remains in Apply PROCESSING")
def step_assert_apply_processing(context: Context) -> None:
    plan = context.plscov3_plan
    assert plan.phase == PlanPhase.APPLY, f"Expected Apply, got {plan.phase}"
    assert plan.processing_state == ProcessingState.PROCESSING, (
        f"Expected PROCESSING, got {plan.processing_state}"
    )


# =================================================================
# _complete_apply_if_queued - async enabled (lines 1599-1603)
# =================================================================


@given("plscov3 an async-enabled service with job store is ready")
def step_svc_async(context: Context) -> None:
    settings = _make_settings()
    settings.async_enabled = True
    job_store = MagicMock()
    context.plscov3_svc = PlanLifecycleService(settings=settings, job_store=job_store)
    context.plscov3_error = None
    context.plscov3_plan = None


@given("plscov3 the current plan is advanced to Apply QUEUED")
def step_advance_apply_queued(context: Context) -> None:
    _advance_to_apply_queued_r3(context)


@then("plscov3 the current plan remains in Apply QUEUED")
def step_assert_apply_queued(context: Context) -> None:
    plan = context.plscov3_plan
    assert plan.phase == PlanPhase.APPLY, f"Expected Apply, got {plan.phase}"
    assert plan.processing_state == ProcessingState.QUEUED, (
        f"Expected QUEUED, got {plan.processing_state}"
    )


# =================================================================
# _complete_apply_if_queued - exception path (lines 1608-1614)
# =================================================================


@when("plscov3 complete_apply_if_queued is invoked with start_apply raising")
def step_caiq_start_apply_raises(context: Context) -> None:
    pid = context.plscov3_plan.identity.plan_id
    svc = context.plscov3_svc

    patcher = patch.object(svc, "start_apply", side_effect=RuntimeError("boom"))
    patcher.start()
    context.add_cleanup(patcher.stop)

    patcher2 = patch.object(svc, "_cleanup_devcontainers")
    patcher2.start()
    context.add_cleanup(patcher2.stop)

    try:
        result = svc._complete_apply_if_queued(pid)
        context.plscov3_plan = result
    except Exception as exc:
        context.plscov3_error = exc


@then("plscov3 the current plan processing state is ERRORED")
def step_assert_errored(context: Context) -> None:
    plan = context.plscov3_plan
    assert plan.processing_state == ProcessingState.ERRORED, (
        f"Expected ERRORED, got {plan.processing_state}"
    )


# =================================================================
# try_auto_run - terminal plan (lines 1687-1690)
# =================================================================


@given("plscov3 the current plan is forced to CANCELLED state")
def step_force_cancelled(context: Context) -> None:
    context.plscov3_plan.processing_state = ProcessingState.CANCELLED


@when("plscov3 try_auto_run is invoked")
def step_call_try_auto_run(context: Context) -> None:
    pid = context.plscov3_plan.identity.plan_id
    svc = context.plscov3_svc

    patcher = patch.object(svc, "_cleanup_devcontainers")
    patcher.start()
    context.add_cleanup(patcher.stop)

    try:
        result = svc.try_auto_run(pid)
        context.plscov3_plan = result
    except Exception as exc:
        context.plscov3_error = exc


@then("plscov3 the current plan is still CANCELLED")
def step_assert_still_cancelled(context: Context) -> None:
    plan = context.plscov3_plan
    assert plan.processing_state == ProcessingState.CANCELLED, (
        f"Expected CANCELLED, got {plan.processing_state}"
    )


# =================================================================
# try_auto_run - full auto with CI profile (lines 1697-1741)
# =================================================================


@given('plscov3 an action with ci automation profile named "{name}" is created')
def step_create_ci_action(context: Context, name: str) -> None:
    _create_action_r3(context, name, automation_profile="ci")


@given(
    'plscov3 that profiled action "{name}" is used on project "{project}" with profile "{profile}"'
)
def step_use_profiled_action(
    context: Context, name: str, project: str, profile: str
) -> None:
    plan = context.plscov3_svc.use_action(
        name, project_links=[ProjectLink(project_name=project)]
    )
    plan.automation_profile = AutomationProfileRef(
        profile_name=profile,
        provenance=AutomationProfileProvenance.ACTION,
    )
    context.plscov3_plan = plan


@then("plscov3 the current plan has reached terminal APPLIED")
def step_assert_terminal_applied(context: Context) -> None:
    assert context.plscov3_error is None, f"Unexpected error: {context.plscov3_error}"
    plan = context.plscov3_plan
    assert plan.processing_state == ProcessingState.APPLIED, (
        f"Expected APPLIED, got {plan.processing_state}"
    )
    assert plan.phase == PlanPhase.APPLY, f"Expected Apply phase, got {plan.phase}"


# =================================================================
# try_auto_run - strategize only with supervised profile (1697-1709)
# =================================================================


@given('plscov3 an action with supervised automation profile named "{name}" is created')
def step_create_supervised_action(context: Context, name: str) -> None:
    _create_action_r3(context, name, automation_profile="supervised")


@then("plscov3 the current plan is in Strategize COMPLETE")
def step_assert_strat_complete(context: Context) -> None:
    plan = context.plscov3_plan
    assert plan.phase == PlanPhase.STRATEGIZE, f"Expected Strategize, got {plan.phase}"
    assert plan.processing_state == ProcessingState.COMPLETE, (
        f"Expected COMPLETE, got {plan.processing_state}"
    )
