"""Step definitions for subplan spawn → execute → merge orchestration.

These steps exercise ``SubplanService.spawn()`` and verify that the
end-to-end orchestration creates real child ``Plan`` domain objects,
wires parent-child identity, inherits project links, and correctly
updates parent ``SubplanStatus`` entries on child completion/failure.
"""

from __future__ import annotations

from unittest.mock import MagicMock

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

from cleveragents.application.services.subplan_service import (
    SpawnEntry,
    SpawnResult,
    SubplanService,
)
from cleveragents.domain.models.core.decision import (
    ContextSnapshot,
    Decision,
    DecisionType,
)
from cleveragents.domain.models.core.plan import (
    ExecutionMode,
    NamespacedName,
    Plan,
    PlanIdentity,
    PlanPhase,
    ProcessingState,
    ProjectLink,
    SubplanConfig,
    SubplanStatus,
)

_PARENT_PLAN_ID: str = "01HGZ6FE0AQDYTR4BXVQZ6PN00"
_ROOT_PLAN_ID: str = "01HGZ6FE0AQDYTR4BXVQZ6RF00"
_DECISION_ID_1: str = "01HGZ6FE0AQDYTR4BXVQZ6DA00"
_DECISION_ID_2: str = "01HGZ6FE0AQDYTR4BXVQZ6DB00"


def _mock_decision_service() -> MagicMock:
    """Create a mock DecisionService for SubplanService construction."""
    svc: MagicMock = MagicMock()
    svc.list_by_type = MagicMock(return_value=[])
    return svc


def _make_parent_plan(
    execution_mode: ExecutionMode = ExecutionMode.SEQUENTIAL,
) -> Plan:
    """Create a parent plan suitable for subplan spawning."""
    return Plan(
        identity=PlanIdentity(
            plan_id=_PARENT_PLAN_ID,
            root_plan_id=_ROOT_PLAN_ID,
        ),
        namespaced_name=NamespacedName(namespace="local", name="parent-plan"),
        description="Parent plan for subplan spawn orchestration test",
        action_name="local/parent-action",
        phase=PlanPhase.STRATEGIZE,
        processing_state=ProcessingState.PROCESSING,
        project_links=[
            ProjectLink(project_name="local/my-project"),
        ],
        subplan_config=SubplanConfig(
            execution_mode=execution_mode,
        ),
    )


def _make_spawn_entries() -> list[SpawnEntry]:
    """Create spawn entries for testing."""
    dec1: Decision = Decision(
        decision_id=_DECISION_ID_1,
        plan_id=_PARENT_PLAN_ID,
        decision_type=DecisionType.SUBPLAN_SPAWN,
        sequence_number=0,
        question="Spawn child plan for module A?",
        chosen_option="local/sub-action-a",
        context_snapshot=ContextSnapshot(),
    )
    dec2: Decision = Decision(
        decision_id=_DECISION_ID_2,
        plan_id=_PARENT_PLAN_ID,
        decision_type=DecisionType.SUBPLAN_SPAWN,
        sequence_number=1,
        question="Spawn child plan for module B?",
        chosen_option="local/sub-action-b",
        context_snapshot=ContextSnapshot(),
    )
    return [
        SpawnEntry(decision=dec1, action_name="local/sub-action-a"),
        SpawnEntry(decision=dec2, action_name="local/sub-action-b"),
    ]


# -- Given steps (not duplicating tdd_subplan_spawn_orchestration_steps) ----


@given("a parent plan configured for parallel subplan spawning")
def step_parallel_parent_plan(context: Context) -> None:
    """Set up a parent plan with parallel subplan configuration."""
    context.parent_plan = _make_parent_plan(
        execution_mode=ExecutionMode.PARALLEL,
    )
    context.subplan_service = SubplanService(
        decision_service=_mock_decision_service(),
    )
    context.subplan_config = SubplanConfig(
        execution_mode=ExecutionMode.PARALLEL,
    )


# -- When steps ---------------------------------------------------------------


@when('a child plan fails with error "{error_msg}"')
def step_child_plan_fails(context: Context, error_msg: str) -> None:
    """Simulate a child plan failure by updating the parent's subplan status."""
    result: SpawnResult = context.spawn_result
    parent: Plan = context.parent_plan

    if result.child_plans:
        child: Plan = result.child_plans[0]
        child_id: str = child.identity.plan_id
        # Update the matching subplan status on the parent
        for status in parent.subplan_statuses:
            if status.subplan_id == child_id:
                status.status = ProcessingState.ERRORED
                status.error = error_msg
                break
        context.affected_child_id = child_id


@when(
    'a child plan completes successfully with changeset summary "{summary}"',
)
def step_child_plan_completes(context: Context, summary: str) -> None:
    """Simulate a child plan completion by updating the parent's subplan status."""
    result: SpawnResult = context.spawn_result
    parent: Plan = context.parent_plan

    if result.child_plans:
        child: Plan = result.child_plans[0]
        child_id: str = child.identity.plan_id
        # Update the matching subplan status on the parent
        for status in parent.subplan_statuses:
            if status.subplan_id == child_id:
                status.status = ProcessingState.COMPLETE
                status.changeset_summary = summary
                status.files_changed = 1
                break
        context.affected_child_id = child_id


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


@then("each child Plan root_plan_id should trace to the root plan")
def step_child_plans_root_id(context: Context) -> None:
    """Assert child Plan objects reference the root plan."""
    result: SpawnResult = context.spawn_result
    assert result.child_plans is not None, "No child_plans on SpawnResult"
    for child in result.child_plans:
        assert isinstance(child, Plan)
        assert child.identity.root_plan_id == _ROOT_PLAN_ID, (
            f"Child plan root_plan_id={child.identity.root_plan_id!r}, "
            f"expected {_ROOT_PLAN_ID!r}"
        )


@then("each child plan should inherit the parent project links")
def step_child_plans_inherit_project_links(context: Context) -> None:
    """Assert child plans carry the parent's project links."""
    result: SpawnResult = context.spawn_result
    parent: Plan = context.parent_plan
    assert result.child_plans is not None, "No child_plans on SpawnResult"
    parent_project_names: list[str] = [
        link.project_name for link in parent.project_links
    ]
    for child in result.child_plans:
        child_project_names: list[str] = [
            link.project_name for link in child.project_links
        ]
        assert child_project_names == parent_project_names, (
            f"Child project links {child_project_names!r} do not match "
            f"parent {parent_project_names!r}"
        )


@then("the parent plan should have pending subplan statuses")
def step_parent_has_pending_statuses(context: Context) -> None:
    """Assert parent plan has queued subplan statuses after spawn."""
    parent: Plan = context.parent_plan
    assert len(parent.subplan_statuses) > 0, "No subplan statuses on parent"
    for status in parent.subplan_statuses:
        assert status.status == ProcessingState.QUEUED, (
            f"Expected QUEUED, got {status.status!r}"
        )


@then("the parent plan subplan_statuses count should equal spawn count")
def step_parent_status_count_matches(context: Context) -> None:
    """Assert the number of subplan statuses equals the spawn entries."""
    parent: Plan = context.parent_plan
    entries: list[SpawnEntry] = context.spawn_entries
    assert len(parent.subplan_statuses) == len(entries), (
        f"Parent has {len(parent.subplan_statuses)} statuses, expected {len(entries)}"
    )


@then("the corresponding parent subplan status should be errored")
def step_parent_status_errored(context: Context) -> None:
    """Assert the affected child's parent subplan status is errored."""
    parent: Plan = context.parent_plan
    child_id: str = context.affected_child_id
    matching: list[SubplanStatus] = [
        s for s in parent.subplan_statuses if s.subplan_id == child_id
    ]
    assert len(matching) == 1, f"Expected 1 matching status, got {len(matching)}"
    assert matching[0].status == ProcessingState.ERRORED


@then('the parent subplan status error should contain "{fragment}"')
def step_parent_status_error_contains(context: Context, fragment: str) -> None:
    """Assert the error message contains the expected fragment."""
    parent: Plan = context.parent_plan
    child_id: str = context.affected_child_id
    matching: list[SubplanStatus] = [
        s for s in parent.subplan_statuses if s.subplan_id == child_id
    ]
    assert len(matching) == 1
    assert matching[0].error is not None, "Error is None"
    assert fragment in matching[0].error, (
        f"Expected '{fragment}' in error '{matching[0].error}'"
    )


@then("the corresponding parent subplan status should be complete")
def step_parent_status_complete(context: Context) -> None:
    """Assert the affected child's parent subplan status is complete."""
    parent: Plan = context.parent_plan
    child_id: str = context.affected_child_id
    matching: list[SubplanStatus] = [
        s for s in parent.subplan_statuses if s.subplan_id == child_id
    ]
    assert len(matching) == 1
    assert matching[0].status == ProcessingState.COMPLETE


@then('the parent subplan status changeset summary should be "{summary}"')
def step_parent_status_changeset_summary(context: Context, summary: str) -> None:
    """Assert the changeset summary matches."""
    parent: Plan = context.parent_plan
    child_id: str = context.affected_child_id
    matching: list[SubplanStatus] = [
        s for s in parent.subplan_statuses if s.subplan_id == child_id
    ]
    assert len(matching) == 1
    assert matching[0].changeset_summary == summary, (
        f"Expected summary '{summary}', got '{matching[0].changeset_summary}'"
    )


@then("the spawn result execution mode should be parallel")
def step_result_mode_parallel(context: Context) -> None:
    """Assert the spawn result execution mode is parallel."""
    result: SpawnResult = context.spawn_result
    assert result.execution_mode == ExecutionMode.PARALLEL, (
        f"Expected PARALLEL, got {result.execution_mode!r}"
    )


@then("the spawn result child plan count should match entries")
def step_result_child_count_matches(context: Context) -> None:
    """Assert child plan count matches spawn entries."""
    result: SpawnResult = context.spawn_result
    entries: list[SpawnEntry] = context.spawn_entries
    assert len(result.child_plans) == len(entries), (
        f"Expected {len(entries)} child plans, got {len(result.child_plans)}"
    )
