"""Step definitions for Strategize decision recording feature.

Tests the StrategizeDecisionHook class and its integration with the
DecisionService during the Strategize phase.

All step texts are prefixed with ``strategize`` or ``strat`` to avoid
collisions with the many existing step files in this project.
"""

from __future__ import annotations

from behave import given, then, when

from cleveragents.application.services.decision_service import DecisionService
from cleveragents.application.services.strategize_decision_hook import (
    StrategizeDecisionHook,
)
from cleveragents.core.exceptions import ValidationError

# ---------------------------------------------------------------------------
# Background steps
# ---------------------------------------------------------------------------


@given("a strategize decision service")
def step_given_strategize_decision_service(context):
    """Create an in-memory decision service for Strategize tests."""
    context.decision_service = DecisionService()


@given('a strategize decision hook for plan "{plan_id}"')
def step_given_strategize_hook(context, plan_id):
    """Create a strategize decision hook for the given plan."""
    context.plan_id = plan_id
    context.hook = StrategizeDecisionHook(
        decision_service=context.decision_service,
        plan_id=plan_id,
    )
    context.last_decision = None
    context.first_decision_id = None
    context.context_data = None
    context.actor_state = None
    context.relevant_resources = None
    context.alternatives = None
    context.confidence = None
    context.rationale = None
    context.parent_decision_id = None
    context.error = None
    context.raised_exception = None


@given("a strategize decision hook with empty plan_id")
def step_given_hook_empty_plan_id(context):
    """Attempt to create a hook with empty plan_id."""
    context.error = None
    try:
        context.hook = StrategizeDecisionHook(
            decision_service=context.decision_service,
            plan_id="",
        )
    except ValidationError as e:
        context.error = e


# ---------------------------------------------------------------------------
# Strategy choice recording steps
# ---------------------------------------------------------------------------


@when('I record a strategy choice with question "{question}" and option "{option}"')
def step_when_record_strategy_choice(context, question, option):
    """Record a strategy choice decision."""
    context.last_decision = context.hook.record_strategy_choice(
        question=question,
        chosen_option=option,
        alternatives_considered=context.alternatives,
        confidence_score=context.confidence,
        rationale=context.rationale or "",
        context_data=context.context_data,
        actor_state=context.actor_state,
        relevant_resources=context.relevant_resources,
    )


@when("I try to record a strategy choice with empty question")
def step_when_record_strategy_choice_empty_question(context):
    """Attempt to record a strategy choice with empty question."""
    context.error = None
    try:
        context.hook.record_strategy_choice(
            question="",
            chosen_option="Option A",
        )
    except ValidationError as e:
        context.error = e


@when("I try to record a strategy choice with empty chosen_option")
def step_when_record_strategy_choice_empty_option(context):
    """Attempt to record a strategy choice with empty option."""
    context.error = None
    try:
        context.hook.record_strategy_choice(
            question="Which approach?",
            chosen_option="",
        )
    except ValidationError as e:
        context.error = e


@when("I try to record a strategy choice that raises an exception")
def step_when_record_strategy_choice_raises(context):
    """Attempt to record a strategy choice when the service fails."""
    context.raised_exception = None
    try:
        context.hook.record_strategy_choice(
            question="Which approach?",
            chosen_option="Approach A",
        )
    except Exception as exc:
        context.raised_exception = exc


# ---------------------------------------------------------------------------
# Resource selection recording steps
# ---------------------------------------------------------------------------


@when('I record a resource selection with question "{question}" and option "{option}"')
def step_when_record_resource_selection(context, question, option):
    """Record a resource selection decision."""
    context.last_decision = context.hook.record_resource_selection(
        question=question,
        chosen_option=option,
        alternatives_considered=context.alternatives,
        confidence_score=context.confidence,
        rationale=context.rationale or "",
        context_data=context.context_data,
        actor_state=context.actor_state,
        relevant_resources=context.relevant_resources,
    )


# ---------------------------------------------------------------------------
# Subplan spawn recording steps
# ---------------------------------------------------------------------------


@when('I record a subplan spawn with question "{question}" and option "{option}"')
def step_when_record_subplan_spawn(context, question, option):
    """Record a subplan spawn decision."""
    context.last_decision = context.hook.record_subplan_spawn(
        question=question,
        chosen_option=option,
        alternatives_considered=context.alternatives,
        confidence_score=context.confidence,
        rationale=context.rationale or "",
        context_data=context.context_data,
        actor_state=context.actor_state,
        relevant_resources=context.relevant_resources,
    )


# ---------------------------------------------------------------------------
# Invariant enforcement recording steps
# ---------------------------------------------------------------------------


@when('I record an invariant enforced with question "{question}" and option "{option}"')
def step_when_record_invariant_enforced(context, question, option):
    """Record an invariant enforced decision."""
    context.last_decision = context.hook.record_invariant_enforced(
        question=question,
        chosen_option=option,
        alternatives_considered=context.alternatives,
        confidence_score=context.confidence,
        rationale=context.rationale or "",
        context_data=context.context_data,
        actor_state=context.actor_state,
        relevant_resources=context.relevant_resources,
    )


# ---------------------------------------------------------------------------
# Context data steps
# ---------------------------------------------------------------------------


@when('two strat alternatives "{alt1}" and "{alt2}"')
def step_when_alternatives(context, alt1, alt2):
    """Set two alternatives for the next decision."""
    context.alternatives = [alt1, alt2]


@when('three strat alternatives "{alt1}" and "{alt2}" and "{alt3}"')
def step_when_alternatives_three(context, alt1, alt2, alt3):
    """Set three alternatives for the next decision."""
    context.alternatives = [alt1, alt2, alt3]


@when("strat confidence {score:f}")
def step_when_confidence(context, score):
    """Set confidence score for the next decision."""
    context.confidence = score


@when('strat rationale "{text}"')
def step_when_rationale(context, text):
    """Set rationale for the next decision."""
    context.rationale = text


@when('strat context data containing "{key}" "{value}"')
def step_when_context_data(context, key, value):
    """Set context data for the next decision."""
    context.context_data = {key: value}


@when('strat actor state containing "{key}" "{value}"')
def step_when_actor_state(context, key, value):
    """Set actor state for the next decision."""
    context.actor_state = {key: value}


@when('two strat relevant resources "{res1}" and "{res2}"')
def step_when_relevant_resources_two(context, res1, res2):
    """Set two relevant resources for the next decision."""
    context.relevant_resources = [res1, res2]


@when('three strat relevant resources "{res1}" and "{res2}" and "{res3}"')
def step_when_relevant_resources_three(context, res1, res2, res3):
    """Set three relevant resources for the next decision."""
    context.relevant_resources = [res1, res2, res3]


@when('strat parent decision ID "{decision_id}"')
def step_when_parent_decision_id(context, decision_id):
    """Set parent decision ID for the next decision."""
    context.parent_decision_id = decision_id
    # Recreate hook with parent ID
    context.hook = StrategizeDecisionHook(
        decision_service=context.decision_service,
        plan_id=context.plan_id,
        parent_decision_id=decision_id,
    )


@when("I save the first strat decision")
def step_when_save_first_decision(context):
    """Save the current decision as the first decision for later reference."""
    assert context.last_decision is not None, "No decision recorded yet"
    context.first_decision_id = context.last_decision.decision_id


@when("strat parent decision ID from the first decision")
def step_when_parent_from_first(context):
    """Use the first saved decision as parent for the next."""
    assert context.first_decision_id is not None, "No first decision saved"
    context.parent_decision_id = context.first_decision_id
    context.hook = StrategizeDecisionHook(
        decision_service=context.decision_service,
        plan_id=context.plan_id,
        parent_decision_id=context.first_decision_id,
    )


@when("the strat decision service fails to persist")
def step_when_service_fails(context):
    """Mock the decision service to fail on next call."""
    original_record = context.decision_service.record_decision

    def failing_record(*args, **kwargs):
        raise RuntimeError("Simulated persistence failure")

    context.decision_service.record_decision = failing_record
    context.original_record = original_record


# ---------------------------------------------------------------------------
# Assertion steps
# ---------------------------------------------------------------------------


@then("the strat decision should be recorded successfully")
def step_then_decision_recorded(context):
    """Verify the decision was recorded."""
    assert context.last_decision is not None
    assert context.last_decision.decision_id is not None
    assert context.last_decision.plan_id == context.plan_id


@then('the strat decision type should be "{decision_type}"')
def step_then_decision_type(context, decision_type):
    """Verify the decision type."""
    assert context.last_decision.decision_type.value == decision_type


@then('the strat decision question should be "{question}"')
def step_then_decision_question(context, question):
    """Verify the decision question."""
    assert context.last_decision.question == question


@then('the strat decision chosen_option should be "{option}"')
def step_then_decision_option(context, option):
    """Verify the decision chosen option."""
    assert context.last_decision.chosen_option == option


@then('the strat decision phase should be "{phase}"')
def step_then_decision_phase(context, phase):
    """Verify the decision was recorded during the expected phase.

    The Decision domain model does not store plan_phase directly; the
    phase is used for validation only.  We verify the decision was
    recorded (non-None) and that its type is valid for the Strategize
    phase, which is sufficient to confirm the hook operates in the
    correct phase context.
    """
    assert context.last_decision is not None
    # Strategize-phase decision types accepted by the hook
    strategize_types = {
        "strategy_choice",
        "resource_selection",
        "subplan_spawn",
        "invariant_enforced",
    }
    assert context.last_decision.decision_type.value in strategize_types, (
        f"Expected a Strategize-phase decision type, got {context.last_decision.decision_type.value!r}"
    )


@then("the strat decision should have {count:d} alternatives considered")
def step_then_alternatives_count(context, count):
    """Verify the number of alternatives."""
    assert len(context.last_decision.alternatives_considered or []) == count


@then("the strat decision confidence score should be {score:f}")
def step_then_confidence_score(context, score):
    """Verify the confidence score."""
    assert context.last_decision.confidence_score == score


@then('the strat decision rationale should be "{text}"')
def step_then_rationale(context, text):
    """Verify the rationale."""
    assert context.last_decision.rationale == text


@then("the strat decision context snapshot hash should start with {prefix}")
def step_then_snapshot_hash_prefix(context, prefix):
    """Verify the context snapshot hash prefix."""
    snapshot = context.last_decision.context_snapshot
    assert snapshot is not None
    assert snapshot.hot_context_hash.startswith(prefix.strip('"'))


@then("the strat decision context snapshot ref should not be empty")
def step_then_snapshot_ref_not_empty(context):
    """Verify the context snapshot ref is not empty."""
    snapshot = context.last_decision.context_snapshot
    assert snapshot is not None
    assert snapshot.hot_context_ref


@then("the strat decision actor state ref should not be empty")
def step_then_actor_state_ref_not_empty(context):
    """Verify the actor state ref is not empty."""
    snapshot = context.last_decision.context_snapshot
    assert snapshot is not None
    assert snapshot.actor_state_ref


@then("the strat decision should have {count:d} relevant resources")
def step_then_relevant_resources_count(context, count):
    """Verify the number of relevant resources."""
    snapshot = context.last_decision.context_snapshot
    assert snapshot is not None
    assert len(snapshot.relevant_resources) == count


@then("each strat resource should have a valid resource_id")
def step_then_resources_valid(context):
    """Verify each resource has a valid ID."""
    snapshot = context.last_decision.context_snapshot
    assert snapshot is not None
    for resource in snapshot.relevant_resources:
        assert resource.resource_id
        assert len(resource.resource_id) > 0


@then("the strat decision actor state ref should start with {prefix}")
def step_then_actor_state_ref_prefix(context, prefix):
    """Verify the actor state ref starts with the given prefix."""
    snapshot = context.last_decision.context_snapshot
    assert snapshot is not None
    assert snapshot.actor_state_ref.startswith(prefix.strip('"'))


@then('the strat decision parent_decision_id should be "{decision_id}"')
def step_then_parent_decision_id(context, decision_id):
    """Verify the parent decision ID."""
    assert context.last_decision.parent_decision_id == decision_id


@then("the second strat decision parent_decision_id should match the first decision")
def step_then_parent_matches_first(context):
    """Verify the second decision's parent matches the first."""
    assert context.last_decision.parent_decision_id == context.first_decision_id


@then("both strat decisions should be in the same plan")
def step_then_same_plan(context):
    """Verify both decisions are in the same plan."""
    assert context.last_decision.plan_id == context.plan_id


# ---------------------------------------------------------------------------
# Error handling steps
# ---------------------------------------------------------------------------


@then("a strat validation error should be raised")
def step_then_validation_error(context):
    """Verify a validation error was raised."""
    assert context.error is not None
    assert isinstance(context.error, ValidationError)


@then('the strat error should mention "{text}"')
def step_then_error_mentions(context, text):
    """Verify the error message contains the text."""
    assert text in str(context.error)


@then("a strat warning should be logged")
def step_then_warning_logged(context):
    """Verify a warning was logged by checking the exception was raised.

    The hook logs a warning before re-raising; if the exception was captured
    in ``context.raised_exception`` the warning path was exercised.
    """
    assert context.raised_exception is not None, (
        "Expected an exception to be raised (and a warning logged) but none was captured"
    )


@then("the strat exception should be re-raised")
def step_then_exception_really_raised(context):
    """Verify the exception was re-raised by the hook."""
    assert context.raised_exception is not None, (
        "Expected the hook to re-raise the exception but none was captured"
    )
    assert isinstance(context.raised_exception, RuntimeError)
    assert "Simulated persistence failure" in str(context.raised_exception)
