"""Step definitions for plan_lifecycle_service_coverage_r2.feature.

Targets uncovered lines in PlanLifecycleService (build/coverage.xml hits=0):
- Lines 226-231: _try_record_decision exception handler
- Line 643:      use_action description fallback to str(namespaced_name)
- Line 1437:     try_auto_revert_from_apply early return (not Apply phase)
- Line 1440:     try_auto_revert_from_apply early return (not CONSTRAINED)
- Lines 1452-1458: try_auto_revert_from_apply loop guard (MAX_REVERSIONS)
- Line 1488:     try_auto_revert_from_execute early return (not Execute phase)
- Lines 1500-1506: try_auto_revert_from_execute loop guard (MAX_REVERSIONS)
"""

from __future__ import annotations

import tempfile
from unittest.mock import MagicMock

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.domain.models.core import Actor
from cleveragents.domain.models.core.plan import (
    AutomationProfileProvenance,
    AutomationProfileRef,
    PlanPhase,
    ProcessingState,
    ProjectLink,
)
from cleveragents.infrastructure.database.unit_of_work import UnitOfWork

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


@given("I have a fresh plan lifecycle service for coverage r2")
def step_create_fresh_service_r2(context: Context) -> None:
    """Create a clean PlanLifecycleService for round-2 coverage tests."""
    Settings._instance = None
    settings = Settings()
    context.service = PlanLifecycleService(settings=settings)
    context.error = None


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


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


def _create_plan_in_phase_r2(
    context: Context,
    target_phase: PlanPhase,
    action_name: str | None = None,
) -> object:
    """Create a plan and advance it to the given phase."""
    aname = action_name or f"local/cov-r2-{id(context)}"
    action = _create_action_r2(context, aname)
    plan = context.service.use_action(
        action_name=str(action.namespaced_name),
        project_links=[ProjectLink(project_name="proj-r2")],
    )
    pid = plan.identity.plan_id

    if target_phase in (PlanPhase.EXECUTE, PlanPhase.APPLY):
        context.service.start_strategize(pid)
        context.service.complete_strategize(pid)
        # complete_strategize calls auto_progress which may call execute_plan
        # Refresh the plan
        plan = context.service.get_plan(pid)
        if plan.phase == PlanPhase.STRATEGIZE:
            context.service.execute_plan(pid)

    if target_phase == PlanPhase.APPLY:
        plan = context.service.get_plan(pid)
        if plan.phase == PlanPhase.EXECUTE:
            context.service.start_execute(pid)
            context.service.complete_execute(pid)
            # complete_execute calls auto_progress which may call apply_plan
            plan = context.service.get_plan(pid)
            if plan.phase == PlanPhase.EXECUTE:
                context.service.apply_plan(pid)

    context.plan = context.service.get_plan(pid)
    return context.plan


# =================================================================
# Scenario: Decision recording failure is silently caught (lines 226-231)
# =================================================================


@given("a plan lifecycle service with a failing decision service")
def step_create_service_with_failing_decision_service(context: Context) -> None:
    """Create a PlanLifecycleService with a DecisionService that raises."""
    Settings._instance = None
    settings = Settings()

    # Create a mock decision service that always raises
    mock_ds = MagicMock()
    mock_ds.record_decision.side_effect = RuntimeError(
        "Simulated decision recording failure"
    )

    context.service = PlanLifecycleService(
        settings=settings,
        decision_service=mock_ds,
    )
    context.mock_decision_service = mock_ds
    context.error = None


@given('an action "{name}" exists for coverage r2')
def step_create_named_action_r2(context: Context, name: str) -> None:
    """Create an action with the given name."""
    context.action = _create_action_r2(context, name)


@given('a plan created from "{action_name}" for coverage r2')
def step_create_plan_from_action_r2(context: Context, action_name: str) -> None:
    """Create a plan from the named action."""
    context.plan = context.service.use_action(
        action_name=action_name,
        project_links=[ProjectLink(project_name="proj-r2")],
    )


@when("I start strategize and the decision service raises an error")
def step_start_strategize_with_failing_ds(context: Context) -> None:
    """Start strategize — the decision service will raise, but it should
    be caught internally and not propagate."""
    context.error = None
    try:
        pid = context.plan.identity.plan_id
        context.plan = context.service.start_strategize(pid)
    except Exception as e:
        context.error = e


@then("the plan should be in processing state without raising an error")
def step_verify_processing_state_no_error(context: Context) -> None:
    """Verify the plan transitioned to PROCESSING and no error propagated."""
    assert context.error is None, f"Expected no error but got: {context.error}"
    assert context.plan.processing_state == ProcessingState.PROCESSING, (
        f"Expected PROCESSING, got {context.plan.processing_state}"
    )
    # Verify the mock was actually called (proving the except block ran)
    assert context.mock_decision_service.record_decision.called, (
        "Expected record_decision to have been called"
    )


# =================================================================
# Scenario: use_action description fallback (line 643)
# =================================================================


@given('an action "{name}" with its description cleared after creation')
def step_create_action_with_blank_description(context: Context, name: str) -> None:
    """Create an action, then clear both description fields to force
    the fallback to str(namespaced_name) on line 643."""
    action = _create_action_r2(context, name)
    # Bypass Pydantic validation to clear the description fields
    action.__dict__["description"] = ""
    action.__dict__["long_description"] = None
    context.action = action
    context.expected_fallback = str(action.namespaced_name)


@when("I use the blank-description action to create a plan for coverage r2")
def step_use_blank_desc_action(context: Context) -> None:
    """Use the action with cleared description fields."""
    context.plan = context.service.use_action(
        action_name=str(context.action.namespaced_name),
        project_links=[ProjectLink(project_name="proj-blank")],
    )


@then("the plan description should equal the action namespaced name string")
def step_verify_description_fallback(context: Context) -> None:
    """Verify the plan description fell back to str(namespaced_name)."""
    assert context.plan.description == context.expected_fallback, (
        f"Expected description '{context.expected_fallback}', "
        f"got '{context.plan.description}'"
    )


# =================================================================
# Scenario: try_auto_revert_from_apply not in Apply phase (line 1437)
# =================================================================


@given("a plan in strategize phase for auto-revert test")
def step_plan_in_strategize_for_revert(context: Context) -> None:
    """Create a plan that is still in Strategize phase."""
    action = _create_action_r2(context, "local/revert-test-1")
    context.plan = context.service.use_action(
        action_name=str(action.namespaced_name),
        project_links=[ProjectLink(project_name="proj-revert")],
    )


@when("I call try_auto_revert_from_apply on the non-apply plan")
def step_try_revert_from_apply_wrong_phase(context: Context) -> None:
    """Call try_auto_revert_from_apply on a plan not in Apply phase."""
    context.result_plan = context.service.try_auto_revert_from_apply(
        context.plan.identity.plan_id,
        reason="test revert",
    )


@then("the plan should remain in strategize phase unchanged")
def step_verify_strategize_unchanged(context: Context) -> None:
    """Verify the plan is still in Strategize phase."""
    assert context.result_plan.phase == PlanPhase.STRATEGIZE, (
        f"Expected STRATEGIZE, got {context.result_plan.phase}"
    )


# =================================================================
# Scenario: try_auto_revert_from_apply not constrained (line 1440)
# =================================================================


@given("a plan in apply phase with queued state for auto-revert test")
def step_plan_in_apply_queued(context: Context) -> None:
    """Create a plan in Apply/QUEUED state (not CONSTRAINED)."""
    _create_plan_in_phase_r2(context, PlanPhase.APPLY)
    # Plan should be in APPLY/QUEUED state after apply_plan


@when("I call try_auto_revert_from_apply on the non-constrained plan")
def step_try_revert_from_apply_not_constrained(context: Context) -> None:
    """Call try_auto_revert_from_apply on a plan that is not constrained."""
    context.result_plan = context.service.try_auto_revert_from_apply(
        context.plan.identity.plan_id,
        reason="test revert not constrained",
    )


@then("the plan should remain in apply phase with queued state")
def step_verify_apply_queued_unchanged(context: Context) -> None:
    """Verify the plan is still in Apply/QUEUED state."""
    assert context.result_plan.phase == PlanPhase.APPLY, (
        f"Expected APPLY, got {context.result_plan.phase}"
    )
    assert context.result_plan.processing_state == ProcessingState.QUEUED, (
        f"Expected QUEUED, got {context.result_plan.processing_state}"
    )


# =================================================================
# Scenario: try_auto_revert_from_apply loop guard (lines 1452-1458)
# =================================================================


@given("a plan in apply constrained state that has reached max reversions")
def step_plan_in_apply_constrained_max_reversions(context: Context) -> None:
    """Create a plan in Apply/CONSTRAINED with reversion_count >= MAX_REVERSIONS."""
    _create_plan_in_phase_r2(context, PlanPhase.APPLY)
    pid = context.plan.identity.plan_id
    plan = context.service.get_plan(pid)

    # Start and process the apply to get to a state where we can constrain
    context.service.start_apply(pid)
    context.service.constrain_apply(pid, "constraint hit")

    # Set reversion_count to MAX_REVERSIONS to trigger the loop guard
    plan = context.service.get_plan(pid)
    plan.reversion_count = plan.MAX_REVERSIONS  # 3
    context.plan = plan


@given("the plan uses a profile that permits auto-reversion from apply")
def step_set_profile_permitting_reversion(context: Context) -> None:
    """Set an automation profile that allows auto-reversion from apply
    (access_network < 1.0 permits automatic reversion)."""
    context.plan.automation_profile = AutomationProfileRef(
        profile_name="ci",  # ci: access_network=0.0 (auto-revert from Apply permitted)
        provenance=AutomationProfileProvenance.PLAN,
    )


@when("I call try_auto_revert_from_apply on the max-reverted plan")
def step_try_revert_from_apply_max_reversions(context: Context) -> None:
    """Call try_auto_revert_from_apply on a plan at MAX_REVERSIONS."""
    context.result_plan = context.service.try_auto_revert_from_apply(
        context.plan.identity.plan_id,
        reason="should be blocked by loop guard",
    )


@then("the plan should remain in apply constrained state due to loop guard")
def step_verify_apply_constrained_blocked(context: Context) -> None:
    """Verify the plan is still in Apply/CONSTRAINED (reversion was blocked)."""
    assert context.result_plan.phase == PlanPhase.APPLY, (
        f"Expected APPLY, got {context.result_plan.phase}"
    )
    assert context.result_plan.processing_state == ProcessingState.CONSTRAINED, (
        f"Expected CONSTRAINED, got {context.result_plan.processing_state}"
    )
    assert context.result_plan.reversion_count == context.result_plan.MAX_REVERSIONS, (
        f"Expected reversion_count={context.result_plan.MAX_REVERSIONS}, "
        f"got {context.result_plan.reversion_count}"
    )


# =================================================================
# Scenario: try_auto_revert_from_execute not Execute phase (line 1488)
# =================================================================


@given("a plan in strategize phase for execute-revert test")
def step_plan_in_strategize_for_execute_revert(context: Context) -> None:
    """Create a plan in Strategize phase for the execute-revert test."""
    action = _create_action_r2(context, "local/exec-revert-1")
    context.plan = context.service.use_action(
        action_name=str(action.namespaced_name),
        project_links=[ProjectLink(project_name="proj-exec-revert")],
    )


@when("I call try_auto_revert_from_execute on the non-execute plan")
def step_try_revert_from_execute_wrong_phase(context: Context) -> None:
    """Call try_auto_revert_from_execute on a plan not in Execute phase."""
    context.result_plan = context.service.try_auto_revert_from_execute(
        context.plan.identity.plan_id,
        reason="test revert from non-execute",
    )


@then("the plan should remain in strategize phase for execute-revert test")
def step_verify_strategize_unchanged_execute_revert(context: Context) -> None:
    """Verify the plan is still in Strategize phase."""
    assert context.result_plan.phase == PlanPhase.STRATEGIZE, (
        f"Expected STRATEGIZE, got {context.result_plan.phase}"
    )


# =================================================================
# Scenario: try_auto_revert_from_execute loop guard (lines 1500-1506)
# =================================================================


@given("a plan in execute phase that has reached max reversions")
def step_plan_in_execute_max_reversions(context: Context) -> None:
    """Create a plan in Execute phase with reversion_count >= MAX_REVERSIONS."""
    _create_plan_in_phase_r2(context, PlanPhase.EXECUTE)
    pid = context.plan.identity.plan_id
    plan = context.service.get_plan(pid)

    # Set reversion_count to MAX_REVERSIONS to trigger the loop guard
    plan.reversion_count = plan.MAX_REVERSIONS  # 3
    context.plan = plan


@given("the plan uses a profile that permits auto-reversion from execute")
def step_set_profile_permitting_execute_reversion(context: Context) -> None:
    """Set an automation profile that allows auto-reversion from execute
    (delete_content < 1.0 permits strategy revision)."""
    context.plan.automation_profile = AutomationProfileRef(
        profile_name="ci",  # ci: delete_content=0.0 (strategy revision permitted)
        provenance=AutomationProfileProvenance.PLAN,
    )


@when("I call try_auto_revert_from_execute on the max-reverted execute plan")
def step_try_revert_from_execute_max_reversions(context: Context) -> None:
    """Call try_auto_revert_from_execute on a plan at MAX_REVERSIONS."""
    context.result_plan = context.service.try_auto_revert_from_execute(
        context.plan.identity.plan_id,
        reason="should be blocked by loop guard",
    )


@then("the plan should remain in execute phase due to loop guard")
def step_verify_execute_blocked(context: Context) -> None:
    """Verify the plan is still in Execute phase (reversion was blocked)."""
    assert context.result_plan.phase == PlanPhase.EXECUTE, (
        f"Expected EXECUTE, got {context.result_plan.phase}"
    )
    assert context.result_plan.reversion_count == context.result_plan.MAX_REVERSIONS, (
        f"Expected reversion_count={context.result_plan.MAX_REVERSIONS}, "
        f"got {context.result_plan.reversion_count}"
    )


# =================================================================
# Scenario: start_strategize resolves estimation actor names (P1-21)
# =================================================================


@given("a persisted plan lifecycle service with actor repository for coverage r2")
def step_create_persisted_service_with_actor_repo(context: Context) -> None:
    """Create PlanLifecycleService backed by a real UnitOfWork/ActorRepository."""
    with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as db_file:
        db_path = db_file.name
    db_url = f"sqlite:///{db_path}"
    uow = UnitOfWork(database_url=db_url)
    uow.init_database()

    Settings._instance = None
    settings = Settings()
    context.service = PlanLifecycleService(settings=settings, unit_of_work=uow)
    context.uow = uow
    context.error = None


@given('a persisted estimation actor config named "{actor_name}"')
def step_create_persisted_estimation_actor_config(
    context: Context, actor_name: str
) -> None:
    """Persist an actor config blob that preflight should resolve by name."""
    config_blob = {
        "name": actor_name,
        "type": "llm",
        "description": "Persisted estimator",
        "model": "gpt-4",
        "role_hint": "estimation",
        "context_view": "strategist",
        "response_format": {"type": "object"},
    }
    actor = Actor(
        name=actor_name,
        provider="local",
        model="gpt-4",
        config_blob=config_blob,
        config_hash=Actor.compute_hash(config_blob),
    )
    with context.uow.transaction() as tx:
        tx.actors.upsert(actor)


@given('an action "{name}" with estimation actor "{estimation_actor}" for coverage r2')
def step_create_named_action_with_estimation_actor(
    context: Context, name: str, estimation_actor: str
) -> None:
    """Create an action configured with a namespaced estimation actor reference."""
    context.action = _create_action_r2(
        context,
        name,
        estimation_actor=estimation_actor,
    )


@when("I start strategize with preflight capture enabled")
def step_start_strategize_with_preflight_capture(context: Context) -> None:
    """Capture actor_registry passed into preflight during start_strategize."""
    original_run_all_checks = context.service.preflight_guardrail.run_all_checks
    context.captured_preflight_kwargs = {}

    def _capturing_run_all_checks(*args: object, **kwargs: object) -> object:
        context.captured_preflight_kwargs = dict(kwargs)
        return original_run_all_checks(*args, **kwargs)

    context.service.preflight_guardrail.run_all_checks = _capturing_run_all_checks

    pid = context.plan.identity.plan_id
    context.plan = context.service.start_strategize(pid)


@then(
    "preflight actor registry should include a resolved estimation actor config payload"
)
def step_verify_preflight_received_resolved_estimation_payload(
    context: Context,
) -> None:
    """Verify estimation actor entry passed to preflight is resolved config dict."""
    kwargs = getattr(context, "captured_preflight_kwargs", {})
    actor_registry = kwargs.get("actor_registry")
    assert isinstance(actor_registry, dict), (
        f"Expected actor_registry dict, got {type(actor_registry).__name__}"
    )
    estimation = actor_registry.get("estimation")
    assert isinstance(estimation, dict), (
        f"Expected resolved estimation config dict, got {type(estimation).__name__}"
    )
    assert estimation.get("role_hint") == "estimation", (
        f"Expected role_hint=estimation in resolved payload, got {estimation}"
    )
    assert isinstance(estimation.get("response_format"), dict), (
        f"Expected response_format dict in resolved payload, got {estimation}"
    )
