"""Step definitions for tdd_plan_explain_plan_id.feature.

TDD issue-capture test for bug #968:  ``plan explain`` expects a decision_id
as its first positional argument, but the M3 acceptance test passes a plan_id.
Because ``svc.get_decision(plan_id)`` raises ``DecisionNotFoundError`` (the
plan ID is not a decision ID), the command exits with rc=1 and "Decision not
found" error.

These steps use the ``@tdd_expected_fail`` tag so that the assertions — which
expect the *fixed* behaviour (rc=0 with decision details) — do not fail CI
while the bug is still unfixed.  Once bug #968 is fixed the tag will be
removed and the tests will run normally.
"""

from __future__ import annotations

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.application.services.decision_service import (
    DecisionNotFoundError,
)
from cleveragents.cli.commands.plan import app as plan_app
from cleveragents.domain.models.core.decision import (
    Decision,
    DecisionType,
)

runner = CliRunner()

_PATCH_CONTAINER = "cleveragents.application.container.get_container"


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


def _make_decision(
    decision_id: str | None = None,
    plan_id: str | None = None,
    sequence: int = 0,
    parent_id: str | None = None,
    dtype: DecisionType = DecisionType.PROMPT_DEFINITION,
    question: str = "What should we build?",
    chosen: str = "A REST API",
) -> Decision:
    """Build a minimal Decision with sensible defaults."""
    did = decision_id or str(ULID())
    pid = plan_id or str(ULID())
    kwargs: dict = {
        "decision_id": did,
        "plan_id": pid,
        "sequence_number": sequence,
        "decision_type": dtype,
        "question": question,
        "chosen_option": chosen,
    }
    if parent_id is not None:
        kwargs["parent_decision_id"] = parent_id
    elif dtype != DecisionType.PROMPT_DEFINITION:
        kwargs["parent_decision_id"] = str(ULID())
    return Decision(**kwargs)


def _mock_container_with_decision_svc(svc_mock: MagicMock) -> MagicMock:
    """Create a mock container whose ``decision_service()`` returns *svc_mock*."""
    container = MagicMock()
    container.decision_service.return_value = svc_mock
    return container


# ---------------------------------------------------------------------------
# Given steps
# ---------------------------------------------------------------------------


@given(
    "tdd968 a mock DecisionService where get_decision raises DecisionNotFoundError for a plan id"
)
def step_tdd968_mock_get_decision_none(context: Context) -> None:
    """Simulate the current buggy behaviour: plan ID is not a decision ID."""
    context.tdd968_plan_id = str(ULID())
    svc = MagicMock()
    # get_decision raises DecisionNotFoundError when called with a plan_id (the bug)
    svc.get_decision.side_effect = DecisionNotFoundError(context.tdd968_plan_id)
    context.tdd968_svc = svc
    context.tdd968_container = _mock_container_with_decision_svc(svc)


@given(
    "tdd968 the same mock DecisionService returns decisions "
    "via list_decisions for the plan id"
)
def step_tdd968_mock_list_decisions(context: Context) -> None:
    """Set up list_decisions to return real decisions for the plan.

    After the fix, the command should fall back to this lookup when
    get_decision raises DecisionNotFoundError.
    """
    root_id = str(ULID())
    child_id = str(ULID())
    pid = context.tdd968_plan_id
    context.tdd968_root_question = "What should we build?"
    decisions = [
        _make_decision(
            decision_id=root_id,
            plan_id=pid,
            sequence=0,
            question=context.tdd968_root_question,
            chosen="A REST API",
        ),
        _make_decision(
            decision_id=child_id,
            plan_id=pid,
            parent_id=root_id,
            sequence=1,
            dtype=DecisionType.STRATEGY_CHOICE,
            question="Which framework?",
            chosen="FastAPI",
        ),
    ]
    context.tdd968_svc.list_decisions.return_value = decisions


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


@when("tdd968 I invoke plan explain with the plan id")
def step_tdd968_invoke_explain(context: Context) -> None:
    """Invoke ``plan explain <plan_id>`` via CliRunner with mocked container."""
    with patch(_PATCH_CONTAINER, return_value=context.tdd968_container):
        result = runner.invoke(
            plan_app,
            ["explain", context.tdd968_plan_id, "--format", "json"],
        )
    context.tdd968_result = result


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


@then("tdd968 the command should exit with return code 0")
def step_tdd968_rc_zero(context: Context) -> None:
    """Assert rc=0 — this will FAIL while bug #968 is unfixed (rc=1)."""
    assert context.tdd968_result.exit_code == 0, (
        f"Expected exit code 0 but got {context.tdd968_result.exit_code}. "
        f"Output: {context.tdd968_result.output}"
    )


@then("tdd968 the output should contain decision details")
def step_tdd968_output_has_details(context: Context) -> None:
    """Assert the output contains decision fields — fails while bug exists."""
    output = context.tdd968_result.output
    # When the bug is fixed, the output should contain decision fields
    assert "decision_id" in output and "question" in output, (
        f"Expected decision details in output, got: {output}"
    )
    # Verify that the fix called list_decisions with the correct plan_id
    context.tdd968_svc.list_decisions.assert_called_once_with(
        context.tdd968_plan_id,
    )


@then("tdd968 the output should contain the root decision question")
def step_tdd968_output_has_question(context: Context) -> None:
    """Assert the root decision question appears — fails while bug exists."""
    output = context.tdd968_result.output
    assert context.tdd968_root_question in output, (
        f"Expected root question '{context.tdd968_root_question}' "
        f"in output, got: {output}"
    )
