"""Step definitions for cross_plan_correction.feature.

Exercises CrossPlanCorrectionService cascade flows, rejection logic,
atomic rollback, model validation, and helper function behaviour.
"""

from __future__ import annotations

from behave import given, then, when
from pydantic import ValidationError as PydanticValidationError

from cleveragents.application.services.cross_plan_correction_service import (
    CrossPlanCorrectionService,
    classify_cascade_action,
)
from cleveragents.core.exceptions import ValidationError
from cleveragents.domain.models.core.correction import (
    CascadeAction,
    CascadeResult,
    ChildPlanState,
    CorrectionImpact,
    CorrectionMode,
    CorrectionRejection,
    CorrectionResult,
    CorrectionStatus,
)

# -------------------------------------------------------------------
# Mock implementations (in-step, minimal — no external mocks needed)
# -------------------------------------------------------------------


class _MockPlanLookup:
    """In-memory child plan state lookup."""

    def __init__(self) -> None:
        self._states: dict[str, ChildPlanState] = {}

    def set_state(self, plan_id: str, state: ChildPlanState) -> None:
        self._states[plan_id] = state

    def get_child_plan_state(self, child_plan_id: str) -> ChildPlanState:
        if child_plan_id not in self._states:
            raise KeyError(f"Unknown child plan: {child_plan_id}")
        return self._states[child_plan_id]


class _MockPlanCanceller:
    """In-memory child plan canceller."""

    def __init__(self, fail_on: str | None = None) -> None:
        self.cancelled: list[str] = []
        self._fail_on = fail_on

    def cancel_child_plan(self, child_plan_id: str) -> None:
        if self._fail_on and child_plan_id == self._fail_on:
            raise RuntimeError(f"Cancel failed for {child_plan_id}")
        self.cancelled.append(child_plan_id)


class _MockSandboxRollbacker:
    """In-memory sandbox rollbacker."""

    def __init__(self) -> None:
        self.rolled_back: list[str] = []

    def rollback_child_plan_sandbox(self, child_plan_id: str) -> None:
        self.rolled_back.append(child_plan_id)


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


@given("a cross-plan correction service")
def step_create_cross_plan_service(context: object) -> None:
    lookup = _MockPlanLookup()
    canceller = _MockPlanCanceller()
    rollbacker = _MockSandboxRollbacker()
    context.lookup = lookup  # type: ignore[attr-defined]
    context.canceller = canceller  # type: ignore[attr-defined]
    context.rollbacker = rollbacker  # type: ignore[attr-defined]
    context.service = CrossPlanCorrectionService(  # type: ignore[attr-defined]
        plan_lookup=lookup,
        plan_canceller=canceller,
        sandbox_rollbacker=rollbacker,
    )
    context.cascade_result = None  # type: ignore[attr-defined]
    context.cascade_error = None  # type: ignore[attr-defined]
    context.error = None  # type: ignore[attr-defined]
    context.correction_with_cascade_result = None  # type: ignore[attr-defined]
    context.classified_action = None  # type: ignore[attr-defined]


@given('a cross-plan correction service with failing canceller on "{fail_id}"')
def step_create_service_failing_canceller(context: object, fail_id: str) -> None:
    lookup = _MockPlanLookup()
    canceller = _MockPlanCanceller(fail_on=fail_id)
    rollbacker = _MockSandboxRollbacker()
    context.lookup = lookup  # type: ignore[attr-defined]
    context.canceller = canceller  # type: ignore[attr-defined]
    context.rollbacker = rollbacker  # type: ignore[attr-defined]
    context.service = CrossPlanCorrectionService(  # type: ignore[attr-defined]
        plan_lookup=lookup,
        plan_canceller=canceller,
        sandbox_rollbacker=rollbacker,
    )
    context.cascade_result = None  # type: ignore[attr-defined]
    context.cascade_error = None  # type: ignore[attr-defined]
    context.error = None  # type: ignore[attr-defined]


@given('the child plan "{plan_id}" has state "{state}"')
def step_set_child_plan_state(context: object, plan_id: str, state: str) -> None:
    context.lookup.set_state(plan_id, ChildPlanState(state))  # type: ignore[attr-defined]


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


@when(
    'I execute a cascade correction for correction "{cid}" with child plans "{plans}"'
)
def step_execute_cascade(context: object, cid: str, plans: str) -> None:
    plan_ids = [p.strip() for p in plans.split(",")]
    context.cascade_result = context.service.execute_cascade(cid, plan_ids)  # type: ignore[attr-defined]


@when(
    'I try to execute a cascade correction for correction "{cid}" with child plans "{plans}"'
)
def step_try_execute_cascade(context: object, cid: str, plans: str) -> None:
    plan_ids = [p.strip() for p in plans.split(",")]
    try:
        context.cascade_result = context.service.execute_cascade(cid, plan_ids)  # type: ignore[attr-defined]
        context.cascade_error = None  # type: ignore[attr-defined]
    except Exception as exc:
        context.cascade_error = exc  # type: ignore[attr-defined]


@when('I evaluate a cascade for correction "{cid}" with no child plans')
def step_evaluate_cascade_empty(context: object, cid: str) -> None:
    context.cascade_result = context.service.evaluate_cascade(cid, [])  # type: ignore[attr-defined]


@when('I evaluate a cascade for correction "{cid}" with child plans "{plans}"')
def step_evaluate_cascade(context: object, cid: str, plans: str) -> None:
    plan_ids = [p.strip() for p in plans.split(",")]
    context.cascade_result = context.service.evaluate_cascade(cid, plan_ids)  # type: ignore[attr-defined]


@when("I try to evaluate a cascade with empty correction_id")
def step_evaluate_empty_cid(context: object) -> None:
    try:
        context.service.evaluate_cascade("", [])  # type: ignore[attr-defined]
        context.error = None  # type: ignore[attr-defined]
    except ValidationError as exc:
        context.error = exc  # type: ignore[attr-defined]


@when('I classify cascade action for state "{state}"')
def step_classify_action(context: object, state: str) -> None:
    context.classified_action = classify_cascade_action(ChildPlanState(state))  # type: ignore[attr-defined]


@when("I try to create a cross-plan service with None plan_lookup")
def step_create_none_lookup(context: object) -> None:
    try:
        CrossPlanCorrectionService(
            plan_lookup=None,  # type: ignore[arg-type]
            plan_canceller=_MockPlanCanceller(),
            sandbox_rollbacker=_MockSandboxRollbacker(),
        )
        context.error = None  # type: ignore[attr-defined]
    except ValidationError as exc:
        context.error = exc  # type: ignore[attr-defined]


@when("I try to create a cross-plan service with None plan_canceller")
def step_create_none_canceller(context: object) -> None:
    try:
        CrossPlanCorrectionService(
            plan_lookup=_MockPlanLookup(),
            plan_canceller=None,  # type: ignore[arg-type]
            sandbox_rollbacker=_MockSandboxRollbacker(),
        )
        context.error = None  # type: ignore[attr-defined]
    except ValidationError as exc:
        context.error = exc  # type: ignore[attr-defined]


@when("I try to create a cross-plan service with None sandbox_rollbacker")
def step_create_none_rollbacker(context: object) -> None:
    try:
        CrossPlanCorrectionService(
            plan_lookup=_MockPlanLookup(),
            plan_canceller=_MockPlanCanceller(),
            sandbox_rollbacker=None,  # type: ignore[arg-type]
        )
        context.error = None  # type: ignore[attr-defined]
    except ValidationError as exc:
        context.error = exc  # type: ignore[attr-defined]


@when(
    'I create a CorrectionRejection with correction_id "{cid}" '
    'and reason "{reason}" and plans "{plans}"'
)
def step_create_rejection_model(
    context: object, cid: str, reason: str, plans: str
) -> None:
    plan_ids = [p.strip() for p in plans.split(",")]
    context.rejection = CorrectionRejection(  # type: ignore[attr-defined]
        correction_id=cid,
        reason=reason,
        affected_applied_child_plan_ids=plan_ids,
    )


@when('I try to create a CascadeAction with invalid action "{action}"')
def step_create_bad_cascade_action(context: object, action: str) -> None:
    try:
        CascadeAction(
            child_plan_id="CP1",
            child_plan_state=ChildPlanState.NOT_STARTED,
            action=action,
        )
        context.error = None  # type: ignore[attr-defined]
    except PydanticValidationError as exc:
        context.error = exc  # type: ignore[attr-defined]


@when(
    'I execute a correction with cascade for correction "{cid}" '
    "with no child plans in revert mode"
)
def step_execute_correction_cascade_no_children(context: object, cid: str) -> None:
    impact = CorrectionImpact(
        affected_decisions=["D1", "D2"],
        affected_child_plans=[],
        risk_level="low",
        rollback_tier="full",
        artifacts_to_archive=["D1.artifact", "D2.artifact"],
    )
    context.correction_with_cascade_result = (  # type: ignore[attr-defined]
        context.service.execute_correction_with_cascade(  # type: ignore[attr-defined]
            cid, impact, CorrectionMode.REVERT
        )
    )


@when(
    'I execute a correction with cascade for correction "{cid}" '
    'with child plan "{plan_id}" in revert mode'
)
def step_execute_correction_cascade_with_child(
    context: object, cid: str, plan_id: str
) -> None:
    impact = CorrectionImpact(
        affected_decisions=["D1"],
        affected_child_plans=[plan_id],
        risk_level="low",
        rollback_tier="full",
        artifacts_to_archive=["D1.artifact"],
    )
    context.correction_with_cascade_result = (  # type: ignore[attr-defined]
        context.service.execute_correction_with_cascade(  # type: ignore[attr-defined]
            cid, impact, CorrectionMode.REVERT
        )
    )


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


@then("the cascade should succeed")
def step_cascade_success(context: object) -> None:
    result: CascadeResult = context.cascade_result  # type: ignore[attr-defined]
    assert result is not None, "No cascade result"
    assert not result.rejected, f"Cascade was rejected: {result.rejection}"


@then("the cascade should be rejected")
def step_cascade_rejected(context: object) -> None:
    result: CascadeResult = context.cascade_result  # type: ignore[attr-defined]
    assert result is not None, "No cascade result"
    assert result.rejected, "Cascade was not rejected"


@then('the cancelled child plans should contain "{plan_id}"')
def step_cancelled_contains(context: object, plan_id: str) -> None:
    result: CascadeResult = context.cascade_result  # type: ignore[attr-defined]
    assert plan_id in result.all_cancelled_plan_ids, (
        f"'{plan_id}' not in cancelled: {result.all_cancelled_plan_ids}"
    )


@then("the cancelled child plans should be empty")
def step_cancelled_empty(context: object) -> None:
    result: CascadeResult = context.cascade_result  # type: ignore[attr-defined]
    assert len(result.all_cancelled_plan_ids) == 0, (
        f"Expected empty cancelled list, got {result.all_cancelled_plan_ids}"
    )


@then('the rolled back child plans should contain "{plan_id}"')
def step_rolled_back_contains(context: object, plan_id: str) -> None:
    result: CascadeResult = context.cascade_result  # type: ignore[attr-defined]
    assert plan_id in result.all_rolled_back_plan_ids, (
        f"'{plan_id}' not in rolled back: {result.all_rolled_back_plan_ids}"
    )


@then("the rolled back child plans should be empty")
def step_rolled_back_empty(context: object) -> None:
    result: CascadeResult = context.cascade_result  # type: ignore[attr-defined]
    assert len(result.all_rolled_back_plan_ids) == 0, (
        f"Expected empty rolled back list, got {result.all_rolled_back_plan_ids}"
    )


@then('the rejection reason should mention "{fragment}"')
def step_rejection_reason_contains(context: object, fragment: str) -> None:
    result: CascadeResult = context.cascade_result  # type: ignore[attr-defined]
    assert result.rejection is not None, "No rejection"
    assert fragment in result.rejection.reason, (
        f"'{fragment}' not in reason: {result.rejection.reason}"
    )


@then('the rejection should list applied child plan "{plan_id}"')
def step_rejection_lists_plan(context: object, plan_id: str) -> None:
    result: CascadeResult = context.cascade_result  # type: ignore[attr-defined]
    assert result.rejection is not None, "No rejection"
    assert plan_id in result.rejection.affected_applied_child_plan_ids, (
        f"'{plan_id}' not in applied IDs: "
        f"{result.rejection.affected_applied_child_plan_ids}"
    )


@then("the cascade should raise an error")
def step_cascade_error(context: object) -> None:
    assert context.cascade_error is not None, "Expected error but none raised"  # type: ignore[attr-defined]


@then('the cascade error should mention "{fragment}"')
def step_cascade_error_message(context: object, fragment: str) -> None:
    assert context.cascade_error is not None, "No error"  # type: ignore[attr-defined]
    assert fragment in str(context.cascade_error), (  # type: ignore[attr-defined]
        f"'{fragment}' not in error: {context.cascade_error}"  # type: ignore[attr-defined]
    )


@then("the cascade evaluation should succeed")
def step_evaluation_success(context: object) -> None:
    result: CascadeResult = context.cascade_result  # type: ignore[attr-defined]
    assert result is not None, "No cascade result"


@then("the cascade actions should be empty")
def step_actions_empty(context: object) -> None:
    result: CascadeResult = context.cascade_result  # type: ignore[attr-defined]
    assert len(result.cascade_actions) == 0, (
        f"Expected empty actions, got {result.cascade_actions}"
    )


@then('the cascade action for "{plan_id}" should be "{action}"')
def step_action_for_plan(context: object, plan_id: str, action: str) -> None:
    result: CascadeResult = context.cascade_result  # type: ignore[attr-defined]
    for ca in result.cascade_actions:
        if ca.child_plan_id == plan_id:
            assert ca.action == action, (
                f"Expected action '{action}' for {plan_id}, got '{ca.action}'"
            )
            return
    raise AssertionError(f"No action found for plan '{plan_id}'")


@then("a cross-plan validation error should be raised")
def step_cross_plan_validation_error(context: object) -> None:
    assert isinstance(context.error, ValidationError), (  # type: ignore[attr-defined]
        f"Expected ValidationError, got {type(context.error)}"  # type: ignore[attr-defined]
    )


@then('the classified action should be "{expected}"')
def step_classified_action(context: object, expected: str) -> None:
    assert context.classified_action == expected, (  # type: ignore[attr-defined]
        f"Expected '{expected}', got '{context.classified_action}'"  # type: ignore[attr-defined]
    )


@then('the rejection correction_id should be "{cid}"')
def step_rejection_cid(context: object, cid: str) -> None:
    assert context.rejection.correction_id == cid  # type: ignore[attr-defined]


@then('the rejection reason should be "{reason}"')
def step_rejection_reason(context: object, reason: str) -> None:
    assert context.rejection.reason == reason  # type: ignore[attr-defined]


@then('the rejection affected plans should contain "{plan_id}"')
def step_rejection_plans_contain(context: object, plan_id: str) -> None:
    assert plan_id in context.rejection.affected_applied_child_plan_ids  # type: ignore[attr-defined]


@then("a cross-plan pydantic validation error should be raised")
def step_pydantic_error(context: object) -> None:
    assert isinstance(context.error, PydanticValidationError), (  # type: ignore[attr-defined]
        f"Expected PydanticValidationError, got {type(context.error)}"  # type: ignore[attr-defined]
    )


@then('the CorrectionStatus enum should include "{value}"')
def step_status_includes(context: object, value: str) -> None:
    values = [s.value for s in CorrectionStatus]
    assert value in values, f"'{value}' not in CorrectionStatus: {values}"


@then("the ChildPlanState enum should have {count:d} members")
def step_child_state_count(context: object, count: int) -> None:
    assert len(ChildPlanState) == count, (
        f"Expected {count} members, got {len(ChildPlanState)}"
    )


@then('the ChildPlanState enum should include "{value}"')
def step_child_state_includes(context: object, value: str) -> None:
    values = [s.value for s in ChildPlanState]
    assert value in values, f"'{value}' not in ChildPlanState: {values}"


@then("the correction with cascade result should be applied")
def step_correction_cascade_applied(context: object) -> None:
    result = context.correction_with_cascade_result  # type: ignore[attr-defined]
    assert isinstance(result, CorrectionResult), (
        f"Expected CorrectionResult, got {type(result)}"
    )
    assert result.status == CorrectionStatus.APPLIED


@then("the correction with cascade result should be a rejection")
def step_correction_cascade_rejection(context: object) -> None:
    result = context.correction_with_cascade_result  # type: ignore[attr-defined]
    assert isinstance(result, CorrectionRejection), (
        f"Expected CorrectionRejection, got {type(result)}"
    )
