"""Step definitions for tui_llm_dispatch.feature.

Tests the TUI LLM dispatch wiring without requiring Textual or importlib.reload.

The core dispatch logic lives in the module-level ``_run_llm_dispatch()``
function in ``tui/app.py``.  This function is Textual-free and can be
imported and tested directly.

Session creation and facade building helpers (``_create_tui_session`` and
``_build_tui_facade`` in ``tui/commands.py``) are also tested directly.
"""

from __future__ import annotations

from typing import Any
from unittest.mock import MagicMock, patch

from behave import given, then, when

from cleveragents.tui.app import _format_worker_outcome, _run_llm_dispatch


# ---------------------------------------------------------------------------
# Shared helpers
# ---------------------------------------------------------------------------


def _make_mock_facade(response_text: str = "Mock LLM reply") -> MagicMock:
    """Return a mock A2aLocalFacade returning a canned assistant message."""
    facade = MagicMock()
    response = MagicMock()
    response.error = None
    response.result = {"assistant_message": response_text}
    facade.dispatch.return_value = response
    return facade


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


@given("the TUI app is initialised with a mock A2A facade and session")
def step_init_with_facade(context: Any) -> None:
    """Set up a mock facade and session_id on the context."""
    context._facade = _make_mock_facade()
    context._session_id = "test-session-001"


@given("the TUI app is initialised with a facade that returns empty response")
def step_init_with_empty_response_facade(context: Any) -> None:
    """Set up a mock facade that returns empty assistant_message."""
    context._facade = _make_mock_facade(response_text="")
    context._session_id = "test-session-001"


@given("the TUI app is initialised without a facade")
def step_init_without_facade(context: Any) -> None:
    """Simulate facade=None (preview fallback path)."""
    context._facade = None
    context._session_id = "default"


@given("the facade raises SessionActorNotConfiguredError")
def step_facade_raises_no_actor(context: Any) -> None:
    """Configure the mock facade to raise SessionActorNotConfiguredError."""
    from cleveragents.domain.models.core.session import SessionActorNotConfiguredError

    facade = MagicMock()
    facade.dispatch.side_effect = SessionActorNotConfiguredError("no actor")
    context._facade = facade
    context._session_id = "default"


@given("the facade raises SessionNotFoundError")
def step_facade_raises_session_not_found(context: Any) -> None:
    """Configure the mock facade to raise SessionNotFoundError."""
    from cleveragents.domain.models.core.session import SessionNotFoundError

    facade = MagicMock()
    facade.dispatch.side_effect = SessionNotFoundError("session not found")
    context._facade = facade
    context._session_id = "default"


@given("the facade raises DatabaseError")
def step_facade_raises_database_error(context: Any) -> None:
    """Configure the mock facade to raise DatabaseError."""
    from cleveragents.core.exceptions import DatabaseError

    facade = MagicMock()
    facade.dispatch.side_effect = DatabaseError("db locked")
    context._facade = facade
    context._session_id = "default"


@given("the facade returns an A2A error response")
def step_facade_returns_error(context: Any) -> None:
    """Configure the mock facade to return an A2A error response."""
    facade = MagicMock()
    error = MagicMock()
    error.message = "internal server error"
    response = MagicMock()
    response.error = error
    response.result = None
    facade.dispatch.return_value = response
    context._facade = facade
    context._session_id = "default"


@given('a mock session service that returns session_id "{sid}"')
def step_mock_session_service(context: Any, sid: str) -> None:
    """Set up a mock session service returning the given session_id."""
    session = MagicMock()
    session.session_id = sid
    service = MagicMock()
    service.create.return_value = session
    context._mock_service = service
    context._expected_sid = sid


@given("the session service raises an exception")
def step_session_service_raises(context: Any) -> None:
    """Set up a mock session service that raises on create()."""
    service = MagicMock()
    service.create.side_effect = RuntimeError("db unavailable")
    context._mock_service = service
    context._expected_sid = "default"


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


@when('I submit the text "{text}"')
def step_submit_text(context: Any, text: str) -> None:
    """Invoke _run_llm_dispatch directly with the given text."""
    context._submitted_text = text  # store for parameterized assertions
    if context._facade is None:
        # Preview fallback path — no dispatch, just echo
        context._result_text = text
        context._app_transcript = []
    else:
        context._result_text = _run_llm_dispatch(
            context._facade, context._session_id, text
        )
        # Simulate what on_input_submitted does: pre-escape and store in transcript
        from rich.markup import escape as _esc

        context._app_transcript = [_esc(f"You: {text}")]


@when('I dispatch "{text}" overriding actor with "{actor}"')
def step_dispatch_text_with_actor(context: Any, text: str, actor: str) -> None:
    """Invoke _run_llm_dispatch with an explicit actor_override."""
    context._submitted_text = text
    context._result_text = _run_llm_dispatch(
        context._facade, context._session_id, text, actor_override=actor
    )


@when("the worker completes with a KeyboardInterrupt error")
def step_worker_keyboard_interrupt(context: Any) -> None:
    """Simulate a worker that completed with a KeyboardInterrupt."""
    context._worker_error = KeyboardInterrupt("user interrupt")


@when("the worker completes with a SystemExit error")
def step_worker_system_exit(context: Any) -> None:
    """Simulate a worker that completed with a SystemExit."""
    context._worker_error = SystemExit(1)


@when("the worker completes with neither result nor error")
def step_worker_no_result_no_error(context: Any) -> None:
    """Simulate a worker that completed with both result and error as None."""
    context._worker_outcome = _format_worker_outcome(result=None, error=None)


@when("_build_tui_facade is called with a broken container")
def step_build_tui_facade_broken_container(context: Any) -> None:
    """Call _build_tui_facade with a container that always raises."""
    from cleveragents.tui import commands as cmd_mod

    cmd_mod._tui_session_service = None
    broken = MagicMock()
    broken.session_service.side_effect = RuntimeError("no db")
    with patch("cleveragents.a2a.cli_bootstrap.get_facade") as mock_facade:
        mock_facade.side_effect = ImportError("no a2a")
        context._facade_result = cmd_mod._build_tui_facade()
    cmd_mod._tui_session_service = None


@when('the worker completes with error "{error_msg}"')
def step_worker_completes_with_error(context: Any, error_msg: str) -> None:
    """Simulate a Textual worker that failed with the given exception."""
    context._worker_outcome = _format_worker_outcome(
        result=None, error=RuntimeError(error_msg)
    )


@when('the worker completes with result "{result}"')
def step_worker_completes_with_result(context: Any, result: str) -> None:
    """Simulate a Textual worker that succeeded with the given result."""
    context._worker_outcome = _format_worker_outcome(result=result, error=None)


@given('a facade returning assistant text "{reply}"')
def step_facade_returning_specific_reply(context: Any, reply: str) -> None:
    """Set up a mock facade with a specific assistant reply."""
    context._facade = _make_mock_facade(response_text=reply)
    context._session_id = "test-session-001"


@given('a persona state with active actor "{actor}"')
def step_persona_state_with_actor(context: Any, actor: str) -> None:
    """Set up a mock PersonaState whose active persona has the given actor."""
    persona = MagicMock()
    persona.actor = actor
    state = MagicMock()
    state.active_persona.return_value = persona
    context._mock_persona_state = state


@when("I call _create_tui_session with that service")
def step_call_create_tui_session(context: Any) -> None:
    """Call _create_tui_session patching get_container, no persona state."""
    from cleveragents.tui import commands as cmd_mod

    container = MagicMock()
    container.session_service.return_value = context._mock_service
    # Reset singleton so the patched container is used on this call.
    cmd_mod._tui_session_service = None
    with patch.object(cmd_mod, "get_container", return_value=container):
        context._result_sid = cmd_mod._create_tui_session()
    cmd_mod._tui_session_service = None  # clean up after test


@when("I call _create_tui_session with that service and persona state")
def step_call_create_tui_session_with_persona(context: Any) -> None:
    """Call _create_tui_session with a persona state so actor is bound."""
    from cleveragents.tui import commands as cmd_mod

    container = MagicMock()
    container.session_service.return_value = context._mock_service
    # Reset singleton so the patched container is used on this call.
    cmd_mod._tui_session_service = None
    with patch.object(cmd_mod, "get_container", return_value=container):
        context._result_sid = cmd_mod._create_tui_session(
            persona_state=context._mock_persona_state
        )
    cmd_mod._tui_session_service = None  # clean up after test


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


@then("the facade should have received a message/send request")
def step_facade_received_request(context: Any) -> None:
    """Assert facade.dispatch was called with method=message/send."""
    context._facade.dispatch.assert_called_once()
    request = context._facade.dispatch.call_args[0][0]
    assert request.method == "message/send", (
        f"Expected method='message/send', got {request.method!r}"
    )
    assert request.params["session_id"] == context._session_id
    assert request.params["message"] == context._submitted_text, (
        f"Expected message={context._submitted_text!r}, "
        f"got {request.params['message']!r}"
    )


@then("the conversation should show the assistant response")
def step_conversation_shows_response(context: Any) -> None:
    """Assert the result contains the assistant response."""
    assert "Mock LLM reply" in context._result_text, (
        f"Expected assistant response in result, got: {context._result_text!r}"
    )


@then('the conversation should show the preview text "{expected}"')
def step_conversation_shows_preview(context: Any, expected: str) -> None:
    """Assert the result is just the echoed input text (no-facade path)."""
    assert expected in context._result_text, (
        f"Expected {expected!r} in result, got: {context._result_text!r}"
    )


@then("the conversation should show the no-actor error message")
def step_conversation_shows_no_actor(context: Any) -> None:
    """Assert the no-actor friendly message is returned."""
    assert "No actor configured" in context._result_text, (
        f"Expected no-actor message in result, got: {context._result_text!r}"
    )


@then("the conversation should show an error message")
def step_conversation_shows_error(context: Any) -> None:
    """Assert an [Error] tag is in the result."""
    assert "[Error]" in context._result_text, (
        f"Expected [Error] in result, got: {context._result_text!r}"
    )


@then('the returned session_id should be "{expected}"')
def step_returned_session_id(context: Any, expected: str) -> None:
    """Assert the session_id from _create_tui_session matches."""
    assert context._result_sid == expected, (
        f"Expected session_id={expected!r}, got {context._result_sid!r}"
    )


@then('the facade should have received a message/send request with actor "{actor}"')
def step_facade_received_request_with_actor(context: Any, actor: str) -> None:
    """Assert the dispatch request included the given actor override."""
    context._facade.dispatch.assert_called_once()
    request = context._facade.dispatch.call_args[0][0]
    assert request.method == "message/send", (
        f"Expected method='message/send', got {request.method!r}"
    )
    assert request.params.get("actor") == actor, (
        f"Expected actor={actor!r} in params, got {request.params.get('actor')!r}"
    )


@then("the conversation should show a session-not-found error message")
def step_conversation_shows_session_not_found(context: Any) -> None:
    """Assert the session-not-found friendly message is returned."""
    assert "Session not found" in context._result_text, (
        f"Expected session-not-found message, got: {context._result_text!r}"
    )


@then("the conversation should show a database error message")
def step_conversation_shows_database_error(context: Any) -> None:
    """Assert the database error message is returned."""
    assert "Database error" in context._result_text, (
        f"Expected database error message, got: {context._result_text!r}"
    )


@then("the formatted outcome should show an error message")
def step_worker_outcome_shows_error(context: Any) -> None:
    """Assert _format_worker_outcome produced an [Error] string."""
    assert context._worker_outcome is not None, "Expected non-None outcome"
    assert "[Error]" in context._worker_outcome, (
        f"Expected [Error] in outcome, got: {context._worker_outcome!r}"
    )


@then('the formatted outcome should be "{expected}"')
def step_worker_outcome_equals(context: Any, expected: str) -> None:
    """Assert _format_worker_outcome returned the expected string."""
    assert context._worker_outcome == expected, (
        f"Expected {expected!r}, got {context._worker_outcome!r}"
    )


@then('the conversation should show "(no response)"')
def step_conversation_shows_no_response(context: Any) -> None:
    """Assert the empty-response fallback text is present."""
    assert "(no response)" in context._result_text, (
        f"Expected '(no response)' in result, got: {context._result_text!r}"
    )


@then("the transcript entry should have markup escaped")
def step_transcript_entry_escaped(context: Any) -> None:
    """Assert the transcript entry stored by on_input_submitted has escaped markup.

    Verifies that _render_transcript receives pre-escaped entries — i.e. that
    on_input_submitted calls _escape() before storing in the transcript, not
    just at render time.  This replaces the previous test that only validated
    escape() in isolation without checking the storage path.
    """
    import re

    transcript = context._app_transcript
    assert transcript, "Expected at least one transcript entry after submit"
    entry = transcript[-1]  # the most recently appended entry

    # The stored entry must NOT contain unescaped Rich markup tags.
    assert not re.search(r"(?<!\\)\[/?[a-zA-Z][^\]]*\]", entry), (
        f"Bug: transcript entry was stored WITHOUT escaping markup: {entry!r}"
    )
    # The text content must still be present after escaping.
    assert "attack" in entry, (
        f"Message content 'attack' must survive markup escaping: {entry!r}"
    )


@then("_format_worker_outcome should re-raise it")
def step_format_worker_outcome_reraises(context: Any) -> None:
    """Assert _format_worker_outcome re-raises BaseException subclasses."""
    raised = False
    err = context._worker_error
    exc_type = type(err)
    try:
        _format_worker_outcome(result=None, error=err)
    except exc_type:
        raised = True
    assert raised, f"_format_worker_outcome must re-raise {exc_type.__name__}"


@then("the formatted outcome should be None")
def step_formatted_outcome_none(context: Any) -> None:
    """Assert _format_worker_outcome returns None when both inputs are None."""
    assert context._worker_outcome is None, (
        f"Expected None from _format_worker_outcome(None, None), "
        f"got: {context._worker_outcome!r}"
    )


@then("the returned facade should be None")
def step_returned_facade_none(context: Any) -> None:
    """Assert _build_tui_facade returns None when construction fails."""
    assert context._facade_result is None, (
        f"Expected _build_tui_facade to return None on failure, "
        f"got: {context._facade_result!r}"
    )


@then('the session should have been created with actor "{actor}"')
def step_session_created_with_actor(context: Any, actor: str) -> None:
    """Assert service.create() was called with the persona's actor."""
    context._mock_service.create.assert_called_once_with(actor_name=actor)
