"""Step definitions for session CLI uncovered-branch coverage tests."""

from __future__ import annotations

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

from behave import given, then, when
from typer.testing import CliRunner

from cleveragents.cli.commands.session import app as session_app
from cleveragents.domain.models.core.session import (
    MessageRole,
    Session,
    SessionExportError,
    SessionMessage,
    SessionNotFoundError,
    SessionService,
    SessionTokenUsage,
)

runner = CliRunner()

# Valid ULIDs for tests (Crockford base32)
_ULID = "01KJ1N8YDN05N29P5RZV4SPDVZ"
_ULID2 = "01KJ1N8YDN05N29P5RZV4SPDW0"
_NOW = datetime(2025, 6, 15, 12, 0, 0)


def _make_session(
    *,
    session_id: str = _ULID,
    actor_name: str | None = None,
    namespace: str = "local",
    messages: list[SessionMessage] | None = None,
    linked_plan_ids: list[str] | None = None,
    token_usage: SessionTokenUsage | None = None,
    metadata: dict[str, Any] | None = None,
) -> Session:
    """Build a Session object with sensible defaults."""
    return Session(
        session_id=session_id,
        actor_name=actor_name,
        namespace=namespace,
        messages=messages or [],
        linked_plan_ids=linked_plan_ids or [],
        token_usage=token_usage or SessionTokenUsage(),
        created_at=_NOW,
        updated_at=_NOW,
        metadata=metadata or {},
    )


def _make_message(
    content: str = "hello",
    role: MessageRole = MessageRole.USER,
    sequence: int = 0,
) -> SessionMessage:
    return SessionMessage(
        message_id=_ULID2,
        role=role,
        content=content,
        sequence=sequence,
        timestamp=_NOW,
    )


def _mock_service() -> MagicMock:
    """Create a MagicMock that passes isinstance checks for SessionService."""
    svc = MagicMock(spec=SessionService)
    return svc


# ---- helpers to patch the module-level _service ---------------------------


def _patch_service(context, svc):
    """Patch session._service and register cleanup."""
    patcher = patch("cleveragents.cli.commands.session._service", svc)
    patcher.start()
    context._cleanup_handlers.append(patcher.stop)


# ===========================================================================
# Scenario: _get_session_service constructs service when _service is None
# ===========================================================================


@given("session cli branch the module-level _service is None")
def step_service_is_none(context):
    import cleveragents.cli.commands.session as mod

    context._original_service = mod._service
    mod._service = None
    context._cleanup_handlers.append(
        lambda: setattr(mod, "_service", context._original_service)
    )


@when("session cli branch I call _get_session_service with mocked container")
def step_call_get_session_service(context):
    import cleveragents.cli.commands.session as mod

    mock_service_instance = MagicMock()
    mock_container = MagicMock()
    mock_container.session_service.return_value = mock_service_instance
    context._mock_persistent_instance = mock_service_instance

    import sys

    mock_container_mod = MagicMock()
    mock_container_mod.get_container = MagicMock(return_value=mock_container)

    with patch.dict(
        sys.modules,
        {
            "cleveragents.application.container": mock_container_mod,
        },
    ):
        result = mod._get_session_service()
        context._get_service_result = result


@then("session cli branch a PersistentSessionService is returned")
def step_persistent_returned(context):
    assert context._get_service_result is context._mock_persistent_instance


# ===========================================================================
# Scenario: _reset_session_service sets _service to None
# ===========================================================================


@given("session cli branch the module-level _service holds a mock")
def step_service_holds_mock(context):
    import cleveragents.cli.commands.session as mod

    context._original_service = mod._service
    mod._service = MagicMock()
    context._cleanup_handlers.append(
        lambda: setattr(mod, "_service", context._original_service)
    )


@when("session cli branch I call _reset_session_service")
def step_call_reset(context):
    import cleveragents.cli.commands.session as mod

    mod._reset_session_service()


@then("session cli branch the module-level _service is None again")
def step_service_is_none_again(context):
    import cleveragents.cli.commands.session as mod

    assert mod._service is None
    # Restore so after_scenario doesn't break
    mod._service = context._original_service


# ===========================================================================
# Scenario: create command catches SessionNotFoundError
# ===========================================================================


@given(
    "session cli branch a mock session service that raises SessionNotFoundError on create"
)
def step_service_raises_on_create(context):
    svc = _mock_service()
    svc.create.side_effect = SessionNotFoundError("actor not found")
    _patch_service(context, svc)


@when("session cli branch I invoke the create command")
def step_invoke_create(context):
    context.result = runner.invoke(session_app, ["create"])


@then("session cli branch the exit code is 1")
def step_exit_code_1(context):
    assert context.result.exit_code == 1, (
        f"Expected exit code 1, got {context.result.exit_code}. "
        f"Output: {context.result.output}"
    )


@then('session cli branch the output contains "{text}"')
def step_output_contains(context, text):
    combined = context.result.output
    assert text in combined, f"Expected '{text}' in output. Got:\n{combined}"


# ===========================================================================
# Scenario: show command with no messages
# ===========================================================================


@given("session cli branch a mock session service returning a session with no messages")
def step_service_no_messages(context):
    session = _make_session(messages=[], linked_plan_ids=[])
    svc = _mock_service()
    svc.get.return_value = session
    _patch_service(context, svc)
    context.session_id = _ULID


@when("session cli branch I invoke the show command for that session")
def step_invoke_show_no_msgs(context):
    context.result = runner.invoke(session_app, ["show", context.session_id])


@then("session cli branch the exit code is 0")
def step_exit_code_0(context):
    assert context.result.exit_code == 0, (
        f"Expected exit code 0, got {context.result.exit_code}. "
        f"Output: {context.result.output}"
    )


@then('session cli branch the output does not contain "{text}"')
def step_output_not_contains(context, text):
    combined = context.result.output
    assert text not in combined, f"Did not expect '{text}' in output. Got:\n{combined}"


# ===========================================================================
# Scenario: show command with linked plan ids
# ===========================================================================


@given(
    "session cli branch a mock session service returning a session with linked plans"
)
def step_service_linked_plans(context):
    session = _make_session(
        linked_plan_ids=["01PLAN1234567890ABCDEFGHIJ", "01PLAN1234567890ABCDEFGHIK"],
    )
    svc = _mock_service()
    svc.get.return_value = session
    _patch_service(context, svc)
    context.session_id_linked = _ULID


@when("session cli branch I invoke the show command for the linked-plans session")
def step_invoke_show_linked(context):
    context.result = runner.invoke(session_app, ["show", context.session_id_linked])


# ===========================================================================
# Scenario: show command with long message content truncated
# ===========================================================================


@given(
    "session cli branch a mock session service returning a session with a long message"
)
def step_service_long_message(context):
    long_content = "A" * 100  # longer than 80 chars → triggers truncation
    msg = _make_message(content=long_content, sequence=0)
    session = _make_session(messages=[msg])
    svc = _mock_service()
    svc.get.return_value = session
    _patch_service(context, svc)
    context.session_id_long = _ULID


@when("session cli branch I invoke the show command for the long-message session")
def step_invoke_show_long(context):
    context.result = runner.invoke(session_app, ["show", context.session_id_long])


# ===========================================================================
# Scenario: delete command aborted via confirmation prompt
# ===========================================================================


@given("session cli branch a mock session service for delete")
def step_service_for_delete(context):
    session = _make_session()
    svc = _mock_service()
    svc.get.return_value = session
    _patch_service(context, svc)
    context.session_id_del = _ULID


@when("session cli branch I invoke the delete command without --yes and answer no")
def step_invoke_delete_no(context):
    # typer.confirm reads from stdin; CliRunner accepts `input` kwarg
    context.result = runner.invoke(
        session_app,
        ["delete", context.session_id_del],
        input="n\n",
    )


@then("session cli branch the delete is aborted")
def step_delete_aborted(context):
    # typer.Abort() sets exit_code to 1
    # The output should contain "Aborted"
    combined = context.result.output
    assert "Aborted" in combined or context.result.exit_code != 0, (
        f"Expected abort. exit_code={context.result.exit_code}, output:\n{combined}"
    )


# ===========================================================================
# Scenario: export command catches SessionExportError
# ===========================================================================


@given(
    "session cli branch a mock session service that raises SessionExportError on export"
)
def step_service_export_error(context):
    svc = _mock_service()
    svc.export_session.side_effect = SessionExportError("checksum mismatch")
    _patch_service(context, svc)


@when("session cli branch I invoke the export command")
def step_invoke_export(context):
    context.result = runner.invoke(session_app, ["export", _ULID])


@then("session cli branch the export exit code is 1")
def step_export_exit_1(context):
    assert context.result.exit_code == 1, (
        f"Expected exit code 1, got {context.result.exit_code}. "
        f"Output: {context.result.output}"
    )


@then('session cli branch the export output contains "{text}"')
def step_export_output_contains(context, text):
    combined = context.result.output
    assert text in combined, f"Expected '{text}' in output. Got:\n{combined}"


# ===========================================================================
# Scenario: tell command with stream=True
# ===========================================================================


@given("session cli branch a mock session service for tell")
def step_service_for_tell(context):
    svc = _mock_service()
    # append_message doesn't need to return anything meaningful for the
    # code path under test — the assistant content is computed inline.
    svc.append_message.return_value = None
    _patch_service(context, svc)


@when("session cli branch I invoke the tell command with --stream")
def step_invoke_tell_stream(context):
    context.result = runner.invoke(
        session_app,
        ["tell", "--session", _ULID, "--stream", "Hello world"],
    )


@then("session cli branch the streamed output contains the assistant response")
def step_streamed_output(context):
    # The assistant content for no actor is: "Acknowledged: Hello world"
    assert "Acknowledged" in context.result.output, (
        f"Expected 'Acknowledged' in output. Got:\n{context.result.output}"
    )
