"""Step definitions for tui_persona_state_coverage.feature.

These steps target specific uncovered lines in tui/persona/state.py:
- Lines 20-21: _resolve_default_name() body
- Line 25:     active_name() cache-miss branch
- Lines 32-33: active_persona() fallback to ensure_default
- Line 39:     set_active_persona() ValueError for unknown persona
- Line 48:     current_preset() cache-miss sets "default"
- Lines 54-55: cycle_preset() when persona has no argument_presets
- Lines 59-60: cycle_preset() when current preset not in names list
- Lines 67-69: effective_arguments() delegation
"""

from types import SimpleNamespace
from unittest.mock import MagicMock

from behave import given, then, when

from cleveragents.tui.persona.schema import Persona, PersonaPreset
from cleveragents.tui.persona.state import PersonaState

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


def _make_persona(
    name: str,
    actor: str = "ns/actor",
    presets: list[PersonaPreset] | None = None,
    base_arguments: dict | None = None,
) -> Persona:
    """Build a real Persona, optionally with custom presets."""
    kwargs: dict = {"name": name, "actor": actor}
    if presets is not None:
        kwargs["argument_presets"] = presets
    if base_arguments is not None:
        kwargs["base_arguments"] = base_arguments
    return Persona(**kwargs)


def _make_mock_registry(
    default_persona: Persona | None = None,
    last_persona: str | None = None,
    get_map: dict[str, Persona | None] | None = None,
) -> MagicMock:
    """Return a MagicMock that quacks like PersonaRegistry."""
    registry = MagicMock()
    dp = default_persona or _make_persona("default")
    registry.ensure_default.return_value = dp
    registry.get_last_persona.return_value = last_persona
    registry.set_last_persona = MagicMock()

    _get_map = get_map or {}

    def _get_side_effect(name: str) -> Persona | None:
        return _get_map.get(name)

    registry.get.side_effect = _get_side_effect
    return registry


# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------


@given("a mock persona registry is prepared")
def step_prepare_mock_registry(context):
    """Prepare a default mock registry; scenarios override as needed."""
    context.mock_registry = _make_mock_registry()
    context.state = PersonaState(registry=context.mock_registry)


# ---------------------------------------------------------------------------
# Scenario: active_name resolves default when session is new (lines 20-21, 25)
# ---------------------------------------------------------------------------


@when('I request the active name for a new session "{session_id}"')
def step_request_active_name_new_session(context, session_id):
    context.result_active_name = context.state.active_name(session_id)


@then("the active name should be the resolved default persona name")
def step_verify_resolved_default_name(context):
    # _resolve_default_name calls ensure_default() then get_last_persona()
    # With last_persona returning None, it falls back to default.name
    assert context.result_active_name == "default"
    context.mock_registry.ensure_default.assert_called()
    context.mock_registry.get_last_persona.assert_called()


# ---------------------------------------------------------------------------
# Scenario: active_name returns cached name on second call
# ---------------------------------------------------------------------------


@given('session "{session_id}" already has active persona "{name}"')
def step_session_has_active_persona(context, session_id, name):
    context.state.active_by_session[session_id] = name


@then('the active name should be "{expected}"')
def step_verify_active_name(context, expected):
    assert context.result_active_name == expected


# ---------------------------------------------------------------------------
# Scenario: active_persona falls back to default (lines 32-33)
# ---------------------------------------------------------------------------


@given('session "{session_id}" is bound to a persona name not in the registry')
def step_session_bound_to_missing_persona(context, session_id):
    # Set session to a name that registry.get() will return None for
    context.state.active_by_session[session_id] = "vanished"
    # Registry get returns None for "vanished", ensure_default returns default
    default_persona = _make_persona("default")
    context.mock_registry = _make_mock_registry(
        default_persona=default_persona,
        get_map={},  # "vanished" not in map → returns None
    )
    context.state = PersonaState(
        registry=context.mock_registry,
        active_by_session={session_id: "vanished"},
    )


@when('I request the active persona for session "{session_id}"')
def step_request_active_persona(context, session_id):
    context.result_persona = context.state.active_persona(session_id)
    context.last_session_id = session_id


@then("the persona should be the registry default")
def step_verify_persona_is_default(context):
    assert context.result_persona.name == "default"


@then("the session should be rebound to the default persona name")
def step_verify_session_rebound(context):
    assert context.state.active_by_session[context.last_session_id] == "default"


# ---------------------------------------------------------------------------
# Scenario: active_persona returns the looked-up persona when found
# ---------------------------------------------------------------------------


@given('session "{session_id}" is bound to a persona name that exists in the registry')
def step_session_bound_to_existing_persona(context, session_id):
    found_persona = _make_persona("researcher", actor="ns/researcher")
    context.mock_registry = _make_mock_registry(
        get_map={"researcher": found_persona},
    )
    context.state = PersonaState(
        registry=context.mock_registry,
        active_by_session={session_id: "researcher"},
    )


@then("the persona should be the one from the registry")
def step_verify_persona_from_registry(context):
    assert context.result_persona.name == "researcher"


# ---------------------------------------------------------------------------
# Scenario: set_active_persona skips preset init when session already has one
# ---------------------------------------------------------------------------


@given('the preset for session "{session_id}" is already set to "{preset_name}"')
def step_preset_already_set(context, session_id, preset_name):
    persona = _make_persona("coder", actor="ns/coder")
    context.mock_registry = _make_mock_registry(get_map={"coder": persona})
    context.state = PersonaState(
        registry=context.mock_registry,
        preset_by_session={session_id: preset_name},
    )
    context._preset_preconfigured = True


@then('the preset for session "{session_id}" should still be "{expected}"')
def step_verify_preset_unchanged(context, session_id, expected):
    assert context.state.preset_by_session[session_id] == expected


# ---------------------------------------------------------------------------
# Scenario: set_active_persona raises ValueError (line 39)
# ---------------------------------------------------------------------------


@when('I try to set an unknown persona "{name}" for session "{session_id}"')
def step_set_unknown_persona(context, name, session_id):
    try:
        context.state.set_active_persona(session_id, name)
        context.caught_error = None
    except ValueError as exc:
        context.caught_error = exc


@then('tpscov a ValueError should be raised with message containing "{fragment}"')
def step_verify_value_error(context, fragment):
    assert context.caught_error is not None, "Expected ValueError but none was raised"
    assert fragment in str(context.caught_error), (
        f"Expected '{fragment}' in '{context.caught_error}'"
    )


# ---------------------------------------------------------------------------
# Scenario: set_active_persona sets and returns a known persona
# ---------------------------------------------------------------------------


@when('I set persona "{name}" for session "{session_id}"')
def step_set_known_persona(context, name, session_id):
    # If a preceding Given step already prepared a state with the right
    # registry (e.g. with preset_by_session pre-populated), reuse it.
    # Otherwise build a fresh state for the persona.
    if not getattr(context, "_preset_preconfigured", False):
        persona = _make_persona(name, actor="ns/coder")
        context.mock_registry = _make_mock_registry(get_map={name: persona})
        context.state = PersonaState(registry=context.mock_registry)
    context.result_persona = context.state.set_active_persona(session_id, name)
    context.last_session_id = session_id


@then('the returned persona name should be "{expected}"')
def step_verify_returned_persona_name(context, expected):
    assert context.result_persona.name == expected


@then('session "{session_id}" should have active persona "{expected}"')
def step_verify_session_active_persona(context, session_id, expected):
    assert context.state.active_by_session[session_id] == expected


@then('the registry last persona should be set to "{expected}"')
def step_verify_last_persona_set(context, expected):
    context.mock_registry.set_last_persona.assert_called_with(expected)


# ---------------------------------------------------------------------------
# Scenario: current_preset returns default for a new session (line 48)
# ---------------------------------------------------------------------------


@when('I request the current preset for new session "{session_id}"')
def step_request_current_preset(context, session_id):
    context.result_preset = context.state.current_preset(session_id)


@then('the current preset should be "{expected}"')
def step_verify_current_preset(context, expected):
    assert context.result_preset == expected


# ---------------------------------------------------------------------------
# Scenario: cycle_preset with no argument presets (lines 54-55)
# ---------------------------------------------------------------------------


@given('a persona with no argument presets is active for session "{session_id}"')
def step_persona_no_presets_active(context, session_id):
    # Build a persona-like object with empty argument_presets to hit lines 54-55.
    # The real Persona validator always adds a default preset, so we use a
    # SimpleNamespace stand-in that active_persona will return.
    fake_persona = SimpleNamespace(
        name="bare",
        argument_presets=[],
        base_arguments={},
    )
    fake_persona.effective_arguments = lambda preset: {}
    registry = MagicMock()
    registry.get.return_value = fake_persona
    registry.ensure_default.return_value = fake_persona
    context.state = PersonaState(
        registry=registry,
        active_by_session={session_id: "bare"},
    )


@when('I cycle the preset for session "{session_id}"')
def step_cycle_preset(context, session_id):
    context.result_cycled_preset = context.state.cycle_preset(session_id)


@then('the cycled preset should be "{expected}"')
def step_verify_cycled_preset(context, expected):
    assert context.result_cycled_preset == expected


# ---------------------------------------------------------------------------
# Scenario: cycle_preset when current preset not in names list (lines 59-60)
# ---------------------------------------------------------------------------


@given('a persona with presets "{preset_csv}" is active for session "{session_id}"')
def step_persona_with_presets_active(context, preset_csv, session_id):
    preset_names = [n.strip() for n in preset_csv.split(",")]
    presets = []
    for pname in preset_names:
        overrides = {} if pname == "default" else {"mode": pname}
        presets.append(PersonaPreset(name=pname, display=pname, overrides=overrides))
    persona = _make_persona("multi", presets=presets, base_arguments={"x": 1})
    registry = MagicMock()
    registry.get.return_value = persona
    registry.ensure_default.return_value = persona
    context.state = PersonaState(
        registry=registry,
        active_by_session={session_id: "multi"},
    )
    context.active_persona_for_test = persona


@given('the current preset for session "{session_id}" is "{preset_name}"')
def step_set_current_preset(context, session_id, preset_name):
    context.state.preset_by_session[session_id] = preset_name


# ---------------------------------------------------------------------------
# Scenario: effective_arguments delegates (lines 67-68-69)
# ---------------------------------------------------------------------------


@given('a persona with base arguments and presets is active for session "{session_id}"')
def step_persona_with_base_args_and_presets(context, session_id):
    presets = [
        PersonaPreset(name="default", display="default", overrides={}),
        PersonaPreset(name="turbo", display="Turbo", overrides={"speed": "fast"}),
    ]
    persona = _make_persona(
        "worker", presets=presets, base_arguments={"model": "gpt-4", "temp": 0.7}
    )
    registry = MagicMock()
    registry.get.return_value = persona
    registry.ensure_default.return_value = persona
    context.state = PersonaState(
        registry=registry,
        active_by_session={session_id: "worker"},
        preset_by_session={session_id: "turbo"},
    )


@when('I request the effective arguments for session "{session_id}"')
def step_request_effective_arguments(context, session_id):
    context.result_eff_args = context.state.effective_arguments(session_id)


@then("the effective arguments should merge base and preset overrides")
def step_verify_effective_arguments(context):
    assert context.result_eff_args["model"] == "gpt-4"
    assert context.result_eff_args["temp"] == 0.7
    assert context.result_eff_args["speed"] == "fast"
