"""Step definitions for plan_correct_tree_wiring.feature.

Verifies that the ``plan correct`` CLI handler resolves the
DecisionService via the DI container, builds the structural tree
and influence DAG, and forwards them to
``CorrectionService.analyze_impact()`` and
``CorrectionService.execute_correction()``.

All step text uses the ``pctw`` prefix to avoid collisions with
other step files.

Fixes: https://git.cleverthis.com/cleveragents/cleveragents-core/issues/606
"""

from __future__ import annotations

from types import SimpleNamespace
from unittest.mock import MagicMock, patch

from behave import given, then, when
from behave.runner import Context
from typer.testing import CliRunner
from ulid import ULID

from cleveragents.cli.commands.plan import app as plan_app

runner = CliRunner()

_PATCH_CONTAINER = "cleveragents.application.container.get_container"

# Fixed ULIDs for deterministic assertions
_PLAN_ID = str(ULID())
_ROOT_ID = "DEC-ROOT-001"
_CHILD_A = "DEC-CHILD-A"
_CHILD_B = "DEC-CHILD-B"
_LEAF_ID = "DEC-LEAF-001"


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


def _make_decision_ns(
    decision_id: str,
    parent_decision_id: str | None,
) -> SimpleNamespace:
    """Create a minimal decision-like namespace for list_decisions."""
    return SimpleNamespace(
        decision_id=decision_id,
        parent_decision_id=parent_decision_id,
    )


def _make_mock_container(
    decisions: list[SimpleNamespace],
    influence_edges: dict[str, list[str]],
) -> MagicMock:
    """Build a mock DI container whose decision_service() returns a DecisionService."""
    mock_decision_svc = MagicMock()
    mock_decision_svc.list_decisions.return_value = decisions
    mock_decision_svc.get_influence_edges.return_value = influence_edges
    mock_container = MagicMock()
    mock_container.decision_service.return_value = mock_decision_svc
    return mock_container


# ---------------------------------------------------------------------------
# GIVEN — three-level tree
# ---------------------------------------------------------------------------


@given("pctw a plan with a three-level decision tree")
def step_pctw_three_level_tree(context: Context) -> None:
    """Set up a mock DecisionService returning a three-level tree."""
    decisions = [
        _make_decision_ns(_ROOT_ID, None),
        _make_decision_ns(_CHILD_A, _ROOT_ID),
        _make_decision_ns(_CHILD_B, _ROOT_ID),
    ]
    influence_edges: dict[str, list[str]] = {_ROOT_ID: [_CHILD_A]}
    context.pctw_mock_container = _make_mock_container(decisions, influence_edges)
    context.pctw_plan_id = _PLAN_ID
    context.pctw_target_id = _ROOT_ID


@given("pctw a plan with a single leaf decision")
def step_pctw_single_leaf(context: Context) -> None:
    """Set up a mock DecisionService returning a lone leaf decision."""
    decisions = [_make_decision_ns(_LEAF_ID, None)]
    context.pctw_mock_container = _make_mock_container(decisions, {})
    context.pctw_plan_id = _PLAN_ID
    context.pctw_target_id = _LEAF_ID


# ---------------------------------------------------------------------------
# GIVEN — CorrectionService with argument capture
# ---------------------------------------------------------------------------


def _make_capturing_correction_svc(
    *,
    capture_analyze: bool = False,
    capture_execute: bool = False,
    affected_decisions: list[str] | None = None,
    target_decision_id: str = _ROOT_ID,
) -> MagicMock:
    """Build a mock CorrectionService that records keyword arguments."""
    svc = MagicMock()

    # request_correction always returns a request-like object
    svc.request_correction.return_value = SimpleNamespace(
        correction_id="CORR-PCTW-01",
        mode=SimpleNamespace(value="revert"),
        target_decision_id=target_decision_id,
        guidance="Fix this",
    )

    affected = affected_decisions or [_ROOT_ID, _CHILD_A, _CHILD_B]
    if capture_analyze:
        impact = SimpleNamespace(
            affected_decisions=affected,
            affected_files=[f"{d}.py" for d in affected],
            estimated_cost=float(len(affected)) * 1.5,
            risk_level="low",
        )
        svc.analyze_impact.return_value = impact

    if capture_execute:
        result = SimpleNamespace(
            correction_id="CORR-PCTW-01",
            status=SimpleNamespace(value="applied"),
            reverted_decisions=affected,
            new_decisions=[],
        )
        svc.execute_correction.return_value = result

    return svc


@given("pctw a CorrectionService that records analyze_impact arguments")
def step_pctw_capture_analyze(context: Context) -> None:
    """Mock CorrectionService recording analyze_impact kwargs."""
    context.pctw_correction_svc = _make_capturing_correction_svc(
        capture_analyze=True,
    )


@given("pctw a CorrectionService that records execute_correction arguments")
def step_pctw_capture_execute(context: Context) -> None:
    """Mock CorrectionService recording execute_correction kwargs."""
    context.pctw_correction_svc = _make_capturing_correction_svc(
        capture_execute=True,
        affected_decisions=[_ROOT_ID, _CHILD_A, _CHILD_B],
    )


# ---------------------------------------------------------------------------
# WHEN — invoke correct
# ---------------------------------------------------------------------------


def _invoke(
    context: Context,
    *,
    dry_run: bool = False,
    target_id: str | None = None,
) -> None:
    """Invoke plan correct with appropriate patches."""
    tid = target_id or context.pctw_target_id
    args = [
        "correct",
        tid,
        "--mode",
        "revert",
        "--guidance",
        "Fix this",
        "--plan",
        context.pctw_plan_id,
    ]
    if dry_run:
        args.append("--dry-run")
    else:
        args.append("--yes")

    context.pctw_mock_container.correction_service.return_value = (
        context.pctw_correction_svc
    )

    with patch(_PATCH_CONTAINER, return_value=context.pctw_mock_container):
        context.pctw_result = runner.invoke(plan_app, args)


@when("pctw I invoke plan correct in dry-run mode")
def step_pctw_invoke_dry_run(context: Context) -> None:
    """Invoke plan correct with --dry-run."""
    _invoke(context, dry_run=True)


@when("pctw I invoke plan correct in execution mode")
def step_pctw_invoke_execute(context: Context) -> None:
    """Invoke plan correct with --yes."""
    _invoke(context, dry_run=False)


@when("pctw I invoke plan correct in dry-run mode for a leaf")
def step_pctw_invoke_dry_run_leaf(context: Context) -> None:
    """Invoke plan correct for a single leaf decision."""
    # Override with single-decision impact and correct target id
    context.pctw_correction_svc = _make_capturing_correction_svc(
        capture_analyze=True,
        affected_decisions=[_LEAF_ID],
        target_decision_id=_LEAF_ID,
    )
    _invoke(context, dry_run=True, target_id=_LEAF_ID)


# ---------------------------------------------------------------------------
# THEN — output assertions
# ---------------------------------------------------------------------------


@then("pctw the dry-run output should list all three affected decisions")
def step_pctw_dryrun_all_three(context: Context) -> None:
    """Verify the dry-run output mentions all three decisions."""
    output = context.pctw_result.output
    assert context.pctw_result.exit_code == 0, (
        f"Expected exit 0, got {context.pctw_result.exit_code}. Output: {output}"
    )
    for did in (_ROOT_ID, _CHILD_A, _CHILD_B):
        assert did in output, f"Expected '{did}' in output: {output}"


@then("pctw the execution output should show reverted decisions")
def step_pctw_exec_shows_reverted(context: Context) -> None:
    """Verify the execution output mentions reverted decisions."""
    output = context.pctw_result.output
    assert context.pctw_result.exit_code == 0, (
        f"Expected exit 0, got {context.pctw_result.exit_code}. Output: {output}"
    )
    assert "Correction applied" in output or "applied" in output.lower(), (
        f"Expected 'applied' in output: {output}"
    )


@then("pctw the dry-run output should list only the target decision")
def step_pctw_dryrun_single(context: Context) -> None:
    """Verify only the target decision appears in dry-run output."""
    output = context.pctw_result.output
    assert context.pctw_result.exit_code == 0, (
        f"Expected exit 0, got {context.pctw_result.exit_code}. Output: {output}"
    )
    assert _LEAF_ID in output, f"Expected '{_LEAF_ID}' in output: {output}"
    assert _ROOT_ID not in output, f"Did NOT expect '{_ROOT_ID}' in output: {output}"


# ---------------------------------------------------------------------------
# THEN — argument forwarding assertions
# ---------------------------------------------------------------------------


@then("pctw analyze_impact received the decision_tree and influence_edges")
def step_pctw_analyze_got_tree(context: Context) -> None:
    """Verify analyze_impact was called with decision_tree and influence_edges."""
    svc = context.pctw_correction_svc
    svc.analyze_impact.assert_called_once()
    call_kwargs = svc.analyze_impact.call_args
    # keyword arguments
    kw = call_kwargs.kwargs if call_kwargs.kwargs else {}
    # Also check positional-keyword mix via call_args[1]
    if not kw:
        kw = call_kwargs[1] if len(call_kwargs) > 1 else {}
    assert "decision_tree" in kw, (
        f"Expected 'decision_tree' kwarg in analyze_impact call. Got: {call_kwargs}"
    )
    assert "influence_edges" in kw, (
        f"Expected 'influence_edges' kwarg in analyze_impact call. Got: {call_kwargs}"
    )
    # Verify the tree contains our parent->children mapping
    tree = kw["decision_tree"]
    assert _ROOT_ID in tree, f"Expected '{_ROOT_ID}' key in tree: {tree}"
    assert _CHILD_A in tree[_ROOT_ID], (
        f"Expected '{_CHILD_A}' in tree['{_ROOT_ID}']: {tree}"
    )
    assert _CHILD_B in tree[_ROOT_ID], (
        f"Expected '{_CHILD_B}' in tree['{_ROOT_ID}']: {tree}"
    )


@then("pctw execute_correction received the decision_tree and influence_edges")
def step_pctw_execute_got_tree(context: Context) -> None:
    """Verify execute_correction was called with decision_tree and influence_edges."""
    svc = context.pctw_correction_svc
    svc.execute_correction.assert_called_once()
    call_kwargs = svc.execute_correction.call_args
    kw = call_kwargs.kwargs if call_kwargs.kwargs else {}
    if not kw:
        kw = call_kwargs[1] if len(call_kwargs) > 1 else {}
    assert "decision_tree" in kw, (
        f"Expected 'decision_tree' kwarg in execute_correction call. Got: {call_kwargs}"
    )
    assert "influence_edges" in kw, (
        f"Expected 'influence_edges' kwarg in execute_correction call. "
        f"Got: {call_kwargs}"
    )


@then("pctw analyze_impact received empty tree and edges")
def step_pctw_analyze_empty_tree(context: Context) -> None:
    """Verify analyze_impact was called with empty tree for a leaf."""
    svc = context.pctw_correction_svc
    svc.analyze_impact.assert_called_once()
    call_kwargs = svc.analyze_impact.call_args
    kw = call_kwargs.kwargs if call_kwargs.kwargs else {}
    if not kw:
        kw = call_kwargs[1] if len(call_kwargs) > 1 else {}
    assert kw.get("decision_tree") == {}, (
        f"Expected empty decision_tree, got: {kw.get('decision_tree')}"
    )
    assert kw.get("influence_edges") == {}, (
        f"Expected empty influence_edges, got: {kw.get('influence_edges')}"
    )
