"""Step definitions for influence_dag_traversal.feature.

Tests the extended BFS that traverses both the structural decision tree
and the influence DAG (``decision_dependencies`` edges) during correction
affected-subtree computation, as well as influence dependency recording
during decision creation.
"""

from __future__ import annotations

from behave import given, then, when
from ulid import ULID

from cleveragents.application.services.correction_service import CorrectionService
from cleveragents.application.services.decision_service import DecisionService
from cleveragents.domain.models.core.correction import CorrectionMode

# A valid ULID used as plan_id in decision recording scenarios.
_TEST_PLAN_ULID = str(ULID())

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


def _parse_csv(text: str) -> list[str]:
    """Split a comma-separated string into a stripped list."""
    return [s.strip() for s in text.split(",") if s.strip()]


# -------------------------------------------------------------------
# Given: service setup
# -------------------------------------------------------------------


@given("an influence-aware correction service")
def step_create_influence_service(context: object) -> None:
    context.service = CorrectionService()  # type: ignore[attr-defined]
    context.correction_id = None  # type: ignore[attr-defined]
    context.decision_tree: dict[str, list[str]] = {}  # type: ignore[attr-defined]
    context.influence_edges: dict[str, list[str]] = {}  # type: ignore[attr-defined]
    context.result = None  # type: ignore[attr-defined]
    context.impact = None  # type: ignore[attr-defined]


@given("a decision service for influence tracking")
def step_create_decision_service(context: object) -> None:
    context.decision_service = DecisionService()  # type: ignore[attr-defined]
    context.recorded_decisions: dict[str, str] = {}  # type: ignore[attr-defined]


# -------------------------------------------------------------------
# Given: correction requests
# -------------------------------------------------------------------


@given(
    'a correction request targeting decision "{decision_id}" '
    'in plan "{plan_id}" for revert'
)
def step_create_revert_request_influence(
    context: object, decision_id: str, plan_id: str
) -> None:
    svc: CorrectionService = context.service  # type: ignore[attr-defined]
    req = svc.request_correction(
        plan_id=plan_id,
        target_decision_id=decision_id,
        mode=CorrectionMode.REVERT,
    )
    context.correction_id = req.correction_id  # type: ignore[attr-defined]


# -------------------------------------------------------------------
# Given: structural trees
# -------------------------------------------------------------------


@given(
    'a structural tree where "{root}" has children "{children_csv}" '
    'and "{mid}" has children "{mid_children_csv}"'
)
def step_structural_tree_two_level(
    context: object,
    root: str,
    children_csv: str,
    mid: str,
    mid_children_csv: str,
) -> None:
    tree: dict[str, list[str]] = context.decision_tree  # type: ignore[attr-defined]
    tree[root] = _parse_csv(children_csv)
    tree[mid] = _parse_csv(mid_children_csv)


@given('a structural tree where "{root}" has children "{children_csv}"')
def step_structural_tree_simple(context: object, root: str, children_csv: str) -> None:
    tree: dict[str, list[str]] = context.decision_tree  # type: ignore[attr-defined]
    tree[root] = _parse_csv(children_csv)


@given('a structural tree with no children for "{node}"')
def step_structural_tree_leaf(context: object, node: str) -> None:
    # Ensure the tree is set but the node has no children (empty).
    # The node will still be found as the BFS root target.
    _ = node  # The tree simply has no entry for this node.
    context.decision_tree = context.decision_tree or {}  # type: ignore[attr-defined]


# -------------------------------------------------------------------
# Given: influence edges
# -------------------------------------------------------------------


@given("no influence edges")
def step_no_influence_edges(context: object) -> None:
    context.influence_edges = {}  # type: ignore[attr-defined]


@given(
    'influence edges where "{src}" influences "{targets_csv}" '
    'and "{src2}" influences "{targets2_csv}"'
)
def step_influence_edges_two_sources(
    context: object,
    src: str,
    targets_csv: str,
    src2: str,
    targets2_csv: str,
) -> None:
    edges: dict[str, list[str]] = context.influence_edges  # type: ignore[attr-defined]
    edges[src] = _parse_csv(targets_csv)
    edges[src2] = _parse_csv(targets2_csv)


@given('influence edges where "{src}" influences "{targets_csv}"')
def step_influence_edges_single(context: object, src: str, targets_csv: str) -> None:
    edges: dict[str, list[str]] = context.influence_edges  # type: ignore[attr-defined]
    edges[src] = _parse_csv(targets_csv)


@given('influence edges with a cycle "{a}" to "{b}" to "{c}" back to "{a_again}"')
def step_influence_cycle(context: object, a: str, b: str, c: str, a_again: str) -> None:
    edges: dict[str, list[str]] = context.influence_edges  # type: ignore[attr-defined]
    edges[a] = [b]
    edges[b] = [c]
    edges[c] = [a_again]


# -------------------------------------------------------------------
# When: impact analysis & execution
# -------------------------------------------------------------------


@when("I analyze the impact with influence support")
def step_analyze_impact_influence(context: object) -> None:
    svc: CorrectionService = context.service  # type: ignore[attr-defined]
    context.impact = svc.analyze_impact(  # type: ignore[attr-defined]
        context.correction_id,  # type: ignore[attr-defined]
        context.decision_tree,  # type: ignore[attr-defined]
        context.influence_edges,  # type: ignore[attr-defined]
    )


@when("I execute the revert with influence support")
def step_execute_revert_influence(context: object) -> None:
    svc: CorrectionService = context.service  # type: ignore[attr-defined]
    context.result = svc.execute_revert(  # type: ignore[attr-defined]
        context.correction_id,  # type: ignore[attr-defined]
        context.decision_tree,  # type: ignore[attr-defined]
        context.influence_edges,  # type: ignore[attr-defined]
    )


# -------------------------------------------------------------------
# When: decision recording
# -------------------------------------------------------------------


@when('I record a root decision "{dtype}" for the tracked plan')
def step_record_root_decision(context: object, dtype: str) -> None:
    svc: DecisionService = context.decision_service  # type: ignore[attr-defined]
    plan_id = _TEST_PLAN_ULID
    decision = svc.record_decision(
        plan_id=plan_id,
        decision_type=dtype,
        question="Root question?",
        chosen_option="Option A",
    )
    recorded: dict[str, str] = context.recorded_decisions  # type: ignore[attr-defined]
    recorded["root"] = decision.decision_id
    context.plan_id = plan_id  # type: ignore[attr-defined]


@when(
    'I record a child decision "implementation_choice" depending on the root decision'
)
def step_record_child_with_dependency(context: object) -> None:
    svc: DecisionService = context.decision_service  # type: ignore[attr-defined]
    recorded: dict[str, str] = context.recorded_decisions  # type: ignore[attr-defined]
    root_id = recorded["root"]
    plan_id: str = context.plan_id  # type: ignore[attr-defined]
    decision = svc.record_decision(
        plan_id=plan_id,
        decision_type="implementation_choice",
        question="Impl question?",
        chosen_option="Option B",
        parent_decision_id=root_id,
        dependency_decision_ids=[root_id],
    )
    recorded["child"] = decision.decision_id


# -------------------------------------------------------------------
# Then: affected decision assertions
# -------------------------------------------------------------------


@then('the affected decision set should be "{expected_csv}"')
def step_assert_affected_decisions(context: object, expected_csv: str) -> None:
    expected = set(_parse_csv(expected_csv))
    impact = context.impact  # type: ignore[attr-defined]
    actual = set(impact.affected_decisions)
    assert actual == expected, f"Expected affected {expected}, got {actual}"


@then("no infinite loop occurred")
def step_assert_no_infinite_loop(context: object) -> None:
    # The fact that we reached this step means BFS terminated.
    # Verify no duplicates in affected list.
    impact = context.impact  # type: ignore[attr-defined]
    decisions = impact.affected_decisions
    assert len(decisions) == len(set(decisions)), (
        f"Duplicate decisions found: {decisions}"
    )


# -------------------------------------------------------------------
# Then: revert result assertions
# -------------------------------------------------------------------


@then('the reverted decisions should include "{decision_id}"')
def step_assert_reverted_contains(context: object, decision_id: str) -> None:
    result = context.result  # type: ignore[attr-defined]
    assert decision_id in result.reverted_decisions, (
        f"Expected {decision_id} in reverted {result.reverted_decisions}"
    )


# -------------------------------------------------------------------
# Then: influence edge assertions
# -------------------------------------------------------------------


@then("the influence edges for the tracked plan should link the root to the child")
def step_assert_influence_edges_present(context: object) -> None:
    svc: DecisionService = context.decision_service  # type: ignore[attr-defined]
    recorded: dict[str, str] = context.recorded_decisions  # type: ignore[attr-defined]
    plan_id: str = context.plan_id  # type: ignore[attr-defined]
    edges = svc.get_influence_edges(plan_id)
    root_id = recorded["root"]
    child_id = recorded["child"]
    assert root_id in edges, f"Expected root {root_id} in edges {edges}"
    assert child_id in edges[root_id], (
        f"Expected child {child_id} in edges[{root_id}]={edges[root_id]}"
    )


@then("the influence edges for the tracked plan should be empty")
def step_assert_influence_edges_empty(context: object) -> None:
    svc: DecisionService = context.decision_service  # type: ignore[attr-defined]
    plan_id: str = context.plan_id  # type: ignore[attr-defined]
    edges = svc.get_influence_edges(plan_id)
    assert edges == {}, f"Expected empty edges, got {edges}"
