"""Step definitions for actor_options_propagation.feature.

Tests that actor-level ``options`` (openai_api_base, openai_api_key,
etc.) are correctly forwarded to ``ProviderRegistry.create_llm()`` across
all call sites: Strategize phase, Execute phase, and SessionWorkflow.

All step text uses the ``aop`` prefix to avoid collisions with other
step files that manipulate ``context.mock_lifecycle`` etc.

Forgejo: #11256
"""

from types import SimpleNamespace
from unittest.mock import MagicMock

from behave import given, then, when

from cleveragents.actor.config import build_llm_kwargs_from_options
from cleveragents.application.services.llm_actors import (
    LLMExecuteActor,
    LLMStrategizeActor,
)
from cleveragents.application.services.strategy_actor import StrategyActor
from cleveragents.core.exceptions import ValidationError

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


def _aop_make_plan(action_name="test-action"):
    return SimpleNamespace(action_name=action_name)


def _aop_make_action(strategy_actor=None, execution_actor=None):
    return SimpleNamespace(
        strategy_actor=strategy_actor,
        execution_actor=execution_actor,
    )


def _aop_llm_response(content):
    return SimpleNamespace(content=content)


def _aop_capturing_registry():
    """Create a MagicMock registry that captures create_llm kwargs."""
    llm = MagicMock()
    llm.invoke.return_value = SimpleNamespace(content="[]")
    captured = {}

    def _fake_create(provider_type=None, model_id=None, **kw):
        captured["provider_type"] = provider_type
        captured["model_id"] = model_id
        captured["kwargs"] = kw
        return llm

    reg = MagicMock()
    reg.create_llm.side_effect = _fake_create
    return reg, captured


# ---------------------------------------------------------------------------
# build_llm_kwargs_from_options shared utility
# ---------------------------------------------------------------------------


@given('aop actor options with "{key}" set to "{value}"')
def step_aop_given_options_with_key(context, key, value):
    if not hasattr(context, "aop_options"):
        context.aop_options = {}
    try:
        context.aop_options[key] = int(value)
    except ValueError:
        try:
            context.aop_options[key] = float(value)
        except ValueError:
            context.aop_options[key] = value


@given("aop actor options is None")
def step_aop_given_options_none(context):
    context.aop_options = None


@given("aop actor options is an empty dict")
def step_aop_given_options_empty(context):
    context.aop_options = {}


@when("I call build_llm_kwargs_from_options")
def step_aop_when_build_llm_kwargs(context):
    context.aop_result = build_llm_kwargs_from_options(context.aop_options)


@then('the result should contain "{key}" with value "{value}"')
def step_aop_then_contains_str(context, key, value):
    kwargs = context.aop_result
    assert key in kwargs, f"Expected '{key}' in result, got {kwargs}"
    assert kwargs[key] == value, f"Expected {key}={value}, got {kwargs[key]}"


@then('the result should contain "{key}" with value {value}')
def step_aop_then_contains_int(context, key, value):
    kwargs = context.aop_result
    assert key in kwargs, f"Expected '{key}' in result, got {kwargs}"
    try:
        expected_val = int(value)
    except ValueError:
        expected_val = float(value)
    assert kwargs[key] == expected_val, (
        f"Expected {key}={expected_val}, got {kwargs[key]}"
    )


@then('the result should not contain "{key}"')
def step_aop_then_not_contains(context, key):
    assert key not in context.aop_result, (
        f"Expected '{key}' NOT in result, got {context.aop_result}"
    )


@then("the result should be an empty dict")
def step_aop_then_empty_dict(context):
    assert context.aop_result == {}, f"Expected empty dict, got {context.aop_result}"


# ---------------------------------------------------------------------------
# StrategyActor options propagation
# ---------------------------------------------------------------------------


@given('aop a mock lifecycle service that resolves actor "{actor_name}"')
def step_aop_given_lifecycle(context, actor_name):
    plan = _aop_make_plan()
    action = _aop_make_action(strategy_actor=actor_name)
    context.aop_lifecycle = SimpleNamespace(
        get_plan=MagicMock(return_value=plan),
        get_action=MagicMock(return_value=action),
        resolve_actor_provider_model=MagicMock(return_value="openai/gpt-4"),
        resolve_actor_options=MagicMock(return_value=None),
    )


@given('aop the resolved actor options contain "{key}" set to "{value}"')
def step_aop_given_options_contain(context, key, value):
    cur = context.aop_lifecycle.resolve_actor_options.return_value or {}
    cur[key] = value
    context.aop_lifecycle.resolve_actor_options.return_value = cur


@given("aop the resolved actor options is None")
def step_aop_given_options_is_none(context):
    context.aop_lifecycle.resolve_actor_options.return_value = None


@given("aop a mock provider registry that captures create_llm kwargs with LLM response")
def step_aop_given_capturing_registry(context):
    reg, captured = _aop_capturing_registry()
    context.aop_registry = reg
    context.aop_captured_kwargs = captured


@when('aop the StrategyActor executes with LLM for plan "{plan_id}"')
def step_aop_when_strategy_actor_executes(context, plan_id):
    actor = StrategyActor(
        provider_registry=context.aop_registry,
        lifecycle_service=context.aop_lifecycle,
    )
    import contextlib

    with contextlib.suppress(
        ValidationError, AttributeError, KeyError, TypeError, ValueError
    ):
        actor.execute(
            plan_id=plan_id,
            definition_of_done="test",
            resources=None,
            project_context=None,
            invariants=None,
        )


@then("aop create_llm should have been called")
def step_aop_then_create_llm_called(context):
    assert context.aop_registry.create_llm.called, (
        "Expected create_llm to have been called"
    )


@then('aop create_llm kwargs should contain "{key}" with value "{value}"')
def step_aop_then_llm_kwargs_contain(context, key, value):
    kwargs = context.aop_captured_kwargs.get("kwargs", {})
    assert key in kwargs, f"Expected '{key}' in create_llm kwargs, got {kwargs}"
    assert kwargs[key] == value, f"Expected {key}={value}, got {kwargs[key]}"


@then('aop create_llm kwargs should not contain "{key}"')
def step_aop_then_llm_kwargs_not_contain(context, key):
    kwargs = context.aop_captured_kwargs.get("kwargs", {})
    assert key not in kwargs, f"Expected '{key}' NOT in create_llm kwargs, got {kwargs}"


# ---------------------------------------------------------------------------
# LLMStrategizeActor options propagation
# ---------------------------------------------------------------------------


@given("aop a valid LLMStrategizeActor with actor options")
def step_aop_given_strategize_with_options(context):
    reg, captured = _aop_capturing_registry()
    context.aop_registry = reg
    context.aop_captured_kwargs = captured
    plan = _aop_make_plan()
    action = _aop_make_action(strategy_actor="local/test-backend")
    context.aop_lifecycle = SimpleNamespace(
        get_plan=MagicMock(return_value=plan),
        get_action=MagicMock(return_value=action),
        resolve_actor_provider_model=MagicMock(return_value="openai/gpt-4"),
        resolve_actor_options=MagicMock(
            return_value={
                "openai_api_base": "http://custom-backend:7000/v1",
                "openai_api_key": "local-key",
            }
        ),
    )
    context.aop_strategize_actor = LLMStrategizeActor(
        provider_registry=context.aop_registry,
        lifecycle_service=context.aop_lifecycle,
    )


@given("aop a valid LLMStrategizeActor without actor options")
def step_aop_given_strategize_without_options(context):
    reg, captured = _aop_capturing_registry()
    context.aop_registry = reg
    context.aop_captured_kwargs = captured
    plan = _aop_make_plan()
    action = _aop_make_action(strategy_actor="openai/gpt-4")
    context.aop_lifecycle = SimpleNamespace(
        get_plan=MagicMock(return_value=plan),
        get_action=MagicMock(return_value=action),
        resolve_actor_provider_model=MagicMock(return_value="openai/gpt-4"),
        resolve_actor_options=MagicMock(return_value=None),
    )
    context.aop_strategize_actor = LLMStrategizeActor(
        provider_registry=context.aop_registry,
        lifecycle_service=context.aop_lifecycle,
    )


@when("aop I call strategize execute with plan_id ")
@when('aop I call strategize execute with plan_id "{plan_id}" and stream callback')
def step_aop_when_strategize_execute(context, plan_id="PLAN_OPT"):
    events = []

    def callback(event_type, data):
        events.append({"type": event_type, "data": data})

    context.aop_strategize_result = context.aop_strategize_actor.execute(
        plan_id=plan_id,
        definition_of_done="Build a REST API",
        stream_callback=callback,
    )


@when('aop I call strategize execute with plan_id "{plan_id}" and no stream callback')
def step_aop_when_strategize_execute_no_cb(context, plan_id):
    context.aop_strategize_result = context.aop_strategize_actor.execute(
        plan_id=plan_id,
        definition_of_done="Build a REST API",
        stream_callback=None,
    )


@then('aop the strategize create_llm should have received "{key}" with value "{value}"')
def step_aop_then_strategize_received(context, key, value):
    kwargs = context.aop_captured_kwargs.get("kwargs", {})
    assert key in kwargs, (
        f"Expected '{key}' in strategize create_llm kwargs, got {kwargs}"
    )
    assert kwargs[key] == value, f"Expected {key}={value}, got {kwargs[key]}"


@then('aop the strategize create_llm should not have received "{key}"')
def step_aop_then_strategize_not_received(context, key):
    kwargs = context.aop_captured_kwargs.get("kwargs", {})
    assert key not in kwargs, (
        f"Expected '{key}' NOT in strategize create_llm kwargs, got {kwargs}"
    )


# ---------------------------------------------------------------------------
# LLMExecuteActor options propagation
# ---------------------------------------------------------------------------


def _aop_executing_registry(content):
    """Create a MagicMock registry + lifecycle for LLMExecuteActor tests."""
    llm_resp = _aop_llm_response(content)
    llm = MagicMock()
    llm.invoke.return_value = llm_resp
    captured = {}

    def _fake_create(provider_type=None, model_id=None, **kw):
        captured["provider_type"] = provider_type
        captured["model_id"] = model_id
        captured["kwargs"] = kw
        return llm

    reg = MagicMock()
    reg.create_llm.side_effect = _fake_create
    return reg, captured


@given("aop a valid LLMExecuteActor with actor options")
def step_aop_given_execute_with_options(context):
    reg, captured = _aop_executing_registry("mock content")
    context.aop_registry = reg
    context.aop_captured_kwargs = captured
    plan = _aop_make_plan()
    action = _aop_make_action(execution_actor="local/test-executor")
    context.aop_lifecycle = SimpleNamespace(
        get_plan=MagicMock(return_value=plan),
        get_action=MagicMock(return_value=action),
        resolve_actor_provider_model=MagicMock(return_value="openai/gpt-4"),
        resolve_actor_options=MagicMock(
            return_value={
                "openai_api_base": "http://custom-executor:9000/v1",
                "openai_api_key": "exec-key",
            }
        ),
    )
    context.aop_execute_actor = LLMExecuteActor(
        provider_registry=context.aop_registry,
        lifecycle_service=context.aop_lifecycle,
    )


@given("aop a valid LLMExecuteActor without actor options")
def step_aop_given_execute_without_options(context):
    reg, captured = _aop_executing_registry("mock content")
    context.aop_registry = reg
    context.aop_captured_kwargs = captured
    plan = _aop_make_plan()
    action = _aop_make_action(execution_actor="openai/gpt-4")
    context.aop_lifecycle = SimpleNamespace(
        get_plan=MagicMock(return_value=plan),
        get_action=MagicMock(return_value=action),
        resolve_actor_provider_model=MagicMock(return_value="openai/gpt-4"),
        resolve_actor_options=MagicMock(return_value=None),
    )
    context.aop_execute_actor = LLMExecuteActor(
        provider_registry=context.aop_registry,
        lifecycle_service=context.aop_lifecycle,
    )


@when('aop I call execute actor with plan_id "{plan_id}" and read_only {read_only}')
def step_aop_when_execute_actor(context, plan_id, read_only):
    ro = read_only in ("True", "true")
    try:
        context.aop_execute_result = context.aop_execute_actor.execute(
            plan_id=plan_id,
            decisions=[],
            sandbox_root=None,
            stream_callback=None,
            read_only=ro,
        )
    except (ValidationError, ValueError, RuntimeError):
        context.aop_execute_result = None


@then('aop the execute create_llm should have received "{key}" with value "{value}"')
def step_aop_then_execute_received(context, key, value):
    kwargs = context.aop_captured_kwargs.get("kwargs", {})
    assert key in kwargs, f"Expected '{key}' in execute create_llm kwargs, got {kwargs}"
    assert kwargs[key] == value, f"Expected {key}={value}, got {kwargs[key]}"


@then('aop the execute create_llm should not have received "{key}"')
def step_aop_then_execute_not_received(context, key):
    kwargs = context.aop_captured_kwargs.get("kwargs", {})
    assert key not in kwargs, (
        f"Expected '{key}' NOT in execute create_llm kwargs, got {kwargs}"
    )
