"""Step definitions for session_workflow.py coverage boost tests."""

from __future__ import annotations

from unittest.mock import MagicMock

from behave import given, then, when
from behave.runner import Context

from cleveragents.application.services.session_caller import (
    LangChainSessionCaller,
    _MinimalStubLLM,
    estimate_cost,
    extract_content,
    extract_token_usage,
    history_to_langchain_messages,
)
from cleveragents.application.services.session_workflow import (
    SessionWorkflow,
)
from cleveragents.domain.models.core.session import (
    MessageRole,
    SessionMessage,
)
from cleveragents.tool.actor_runtime import LLMResponse


@given("the session workflow coverage environment is set up")
def step_sw_coverage_setup(context: Context) -> None:
    """No special setup needed."""
    pass


# ====================================================================
# _extract_content
# ====================================================================


@given('coverage boost a mock response with content "hello world"')
def step_cb_mock_content(context: Context) -> None:
    resp = MagicMock()
    resp.content = "hello world"
    resp.text = "ignored"
    context.cb_mock_response = resp


@given('coverage boost a mock response with text "from text attr" and no content')
def step_cb_text_fallback(context: Context) -> None:
    class _TextOnly:
        def __init__(self) -> None:
            self.text = "from text attr"

    context.cb_mock_response = _TextOnly()


@given(
    "coverage boost a mock response with list content containing text dicts and plain strings"
)
def step_cb_list_content(context: Context) -> None:
    resp = MagicMock()
    resp.content = [
        {"text": "hello"},
        "world",
        {"content": "test"},
    ]
    context.cb_mock_response = resp


@given("coverage boost a mock response with no content or text attribute")
def step_cb_no_attrs(context: Context) -> None:
    class _Unknown:
        pass

    context.cb_mock_response = _Unknown()


@when("coverage boost _extract_content is called")
def step_cb_call_extract_content(context: Context) -> None:
    context.cb_extract_result = extract_content(context.cb_mock_response)


@then('coverage boost the extracted result should be "{expected}"')
def step_cb_extract_result_is(context: Context, expected: str) -> None:
    assert context.cb_extract_result == expected, (
        f"Expected {expected!r}, got {context.cb_extract_result!r}"
    )


@then("coverage boost the result should contain the concatenated texts")
def step_cb_result_concat(context: Context) -> None:
    assert "hello" in context.cb_extract_result
    assert "world" in context.cb_extract_result
    assert "test" in context.cb_extract_result


@then("coverage boost the result should be a string")
def step_cb_result_str(context: Context) -> None:
    assert isinstance(context.cb_extract_result, str)


# ====================================================================
# _extract_token_usage
# ====================================================================


@given(
    "coverage boost a mock response with response_metadata usage input_tokens=100 output_tokens=50"
)
def step_cb_usage_metadata(context: Context) -> None:
    resp = MagicMock()
    resp.response_metadata = {"usage": {"input_tokens": 100, "output_tokens": 50}}
    resp.usage_metadata = {}
    context.cb_mock_response = resp


@given(
    "coverage boost a mock response with response_metadata token_usage input_tokens=200 output_tokens=100"
)
def step_cb_token_usage_metadata(context: Context) -> None:
    resp = MagicMock()
    resp.response_metadata = {
        "token_usage": {"input_tokens": 200, "output_tokens": 100}
    }
    resp.usage_metadata = {}
    context.cb_mock_response = resp


@given(
    "coverage boost a mock response with response_metadata usage prompt_tokens=300 completion_tokens=150"
)
def step_cb_prompt_completion(context: Context) -> None:
    resp = MagicMock()
    resp.response_metadata = {"usage": {"prompt_tokens": 300, "completion_tokens": 150}}
    resp.usage_metadata = {}
    context.cb_mock_response = resp


@given(
    "coverage boost a mock response with usage_metadata input_tokens=400 output_tokens=200"
)
def step_cb_usage_meta_attr(context: Context) -> None:
    resp = MagicMock()
    resp.response_metadata = {}
    resp.usage_metadata = {"input_tokens": 400, "output_tokens": 200}
    context.cb_mock_response = resp


@given("coverage boost a mock response with no usage metadata")
def step_cb_no_metadata(context: Context) -> None:
    resp = MagicMock()
    resp.response_metadata = {}
    resp.usage_metadata = {}
    context.cb_mock_response = resp


@when("coverage boost _extract_token_usage is called")
def step_cb_call_extract_token_usage(context: Context) -> None:
    context.cb_token_input, context.cb_token_output = extract_token_usage(
        context.cb_mock_response
    )


@then(
    "coverage boost input tokens should be {input_tokens:d} and output tokens should be {output_tokens:d}"
)
def step_cb_assert_token_counts(
    context: Context, input_tokens: int, output_tokens: int
) -> None:
    assert context.cb_token_input == input_tokens, (
        f"Expected {input_tokens} input tokens, got {context.cb_token_input}"
    )
    assert context.cb_token_output == output_tokens, (
        f"Expected {output_tokens} output tokens, got {context.cb_token_output}"
    )


# ====================================================================
# _estimate_cost
# ====================================================================


@given(
    "coverage boost input tokens {input_tokens:d} and output tokens {output_tokens:d}"
)
def step_cb_given_tokens(
    context: Context, input_tokens: int, output_tokens: int
) -> None:
    context.cb_input_tokens = input_tokens
    context.cb_output_tokens = output_tokens


@when("coverage boost _estimate_cost is called")
def step_cb_call_estimate_cost(context: Context) -> None:
    context.cb_estimated_cost = estimate_cost(
        context.cb_input_tokens, context.cb_output_tokens
    )


@then("coverage boost the estimated cost should be positive")
def step_cb_cost_positive(context: Context) -> None:
    assert context.cb_estimated_cost > 0.0, (
        f"Expected positive cost, got {context.cb_estimated_cost}"
    )


# ====================================================================
# _history_to_langchain_messages
# ====================================================================


@given("coverage boost session messages with roles SYSTEM, USER, ASSISTANT, and TOOL")
def step_cb_messages_all_roles(context: Context) -> None:
    context.cb_history_messages = [
        SessionMessage(
            message_id="01KJ1N8YDN05N29P5RZV4SPDVZ",
            role=MessageRole.SYSTEM,
            content="You are helpful.",
            sequence=1,
            timestamp="2026-01-01T00:00:00Z",
        ),
        SessionMessage(
            message_id="01KJ1N8YDN05N29P5RZV4SPDW0",
            role=MessageRole.USER,
            content="Hello",
            sequence=2,
            timestamp="2026-01-01T00:00:00Z",
        ),
        SessionMessage(
            message_id="01KJ1N8YDN05N29P5RZV4SPDW1",
            role=MessageRole.ASSISTANT,
            content="Hi!",
            sequence=3,
            timestamp="2026-01-01T00:00:00Z",
        ),
        SessionMessage(
            message_id="01KJ1N8YDN05N29P5RZV4SPDW2",
            role=MessageRole.TOOL,
            content="result",
            sequence=4,
            timestamp="2026-01-01T00:00:00Z",
            tool_call_id="tc1",
        ),
    ]


@when("coverage boost _history_to_langchain_messages is called")
def step_cb_call_history_to_lc(context: Context) -> None:
    context.cb_lc_messages = history_to_langchain_messages(context.cb_history_messages)


@then(
    "coverage boost the result should contain SystemMessage, HumanMessage, AIMessage, and ToolMessage"
)
def step_cb_assert_lc_types(context: Context) -> None:
    from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
    from langchain_core.messages.tool import ToolMessage

    types = [type(m) for m in context.cb_lc_messages]
    assert SystemMessage in types, f"Missing SystemMessage in {types}"
    assert HumanMessage in types, f"Missing HumanMessage in {types}"
    assert AIMessage in types, f"Missing AIMessage in {types}"
    assert ToolMessage in types, f"Missing ToolMessage in {types}"


@given("coverage boost a session message with an unknown role")
def step_cb_message_unknown_role(context: Context) -> None:
    msg = MagicMock(spec=SessionMessage)
    msg.role = "NOT_A_REAL_ROLE"
    msg.content = "test content"
    msg.tool_call_id = None
    context.cb_history_messages = [msg]


@then("coverage boost the result should contain a HumanMessage")
def step_cb_result_contains_human(context: Context) -> None:
    from langchain_core.messages import HumanMessage

    assert len(context.cb_lc_messages) > 0, "Expected at least one message"
    assert isinstance(context.cb_lc_messages[0], HumanMessage), (
        f"Expected HumanMessage, got {type(context.cb_lc_messages[0])}"
    )


@given("coverage boost an empty list of session messages")
def step_cb_empty_history(context: Context) -> None:
    context.cb_history_messages = []


@then("coverage boost the result should be an empty list")
def step_cb_empty_list(context: Context) -> None:
    assert context.cb_lc_messages == [], (
        f"Expected empty list, got {context.cb_lc_messages}"
    )


# ====================================================================
# LangChainSessionCaller.invoke() tool_results
# ====================================================================


@given("coverage boost a LangChainSessionCaller with a stub LLM and empty history")
def step_cb_caller_with_stub(context: Context) -> None:
    stub_llm = MagicMock()
    stub_llm.invoke.return_value = MagicMock(
        content="response text",
        tool_calls=[],
        response_metadata={},
        usage_metadata={},
    )
    context.cb_caller = LangChainSessionCaller(llm=stub_llm, history=[])
    context.cb_stub_llm = stub_llm


@when(
    "coverage boost invoke is called with tool_results containing one success and one failure"
)
def step_cb_invoke_tool_results(context: Context) -> None:
    context.cb_caller.invoke(prompt="test prompt", tool_schemas=[])
    context.cb_tool_result_response = context.cb_caller.invoke(
        prompt="test prompt",
        tool_schemas=[],
        tool_results=[
            {
                "success": True,
                "output": {"result": "ok"},
                "call_id": "c1",
                "tool_name": "tool1",
            },
            {
                "success": False,
                "error": "failed",
                "call_id": "c2",
                "tool_name": "tool2",
            },
        ],
    )


@then("coverage boost the accumulated messages should include tool result messages")
def step_cb_accumulated_has_tool_results(context: Context) -> None:
    from langchain_core.messages.tool import ToolMessage

    tool_msgs = [
        m for m in context.cb_caller._accumulated if isinstance(m, ToolMessage)
    ]
    assert len(tool_msgs) >= 2, f"Expected >= 2 ToolMessages, got {len(tool_msgs)}"


# ====================================================================
# LangChainSessionCaller.invoke() with tool_calls in response
# ====================================================================


@given(
    "coverage boost a LangChainSessionCaller with a stub LLM that returns tool calls"
)
def step_cb_caller_tool_calls(context: Context) -> None:
    stub_llm = MagicMock()
    resp = MagicMock(
        content="response with tool calls",
        tool_calls=[
            {"name": "test_tool", "args": {"arg1": "val1"}, "id": "call_123"},
        ],
        response_metadata={},
        usage_metadata={},
    )
    stub_llm.invoke.return_value = resp
    context.cb_caller = LangChainSessionCaller(llm=stub_llm, history=[])


@when("coverage boost invoke is called for the first time")
def step_cb_invoke_first_time(context: Context) -> None:
    context.cb_first_invoke_result = context.cb_caller.invoke(
        prompt="test prompt", tool_schemas=[]
    )


@then("coverage boost the LLMResponse should contain the extracted tool calls")
def step_cb_llm_response_has_tool_calls(context: Context) -> None:
    assert isinstance(context.cb_first_invoke_result, LLMResponse)
    assert len(context.cb_first_invoke_result.tool_calls) >= 1, (
        f"Expected >= 1 tool calls, got {len(context.cb_first_invoke_result.tool_calls)}"
    )
    tc = context.cb_first_invoke_result.tool_calls[0]
    assert tc.name == "test_tool"
    assert tc.arguments == {"arg1": "val1"}


# ====================================================================
# _build_lc_messages_from_history  (SessionWorkflow method)
# ====================================================================


@given("coverage boost a SessionWorkflow with a stub service and no registry")
def step_cb_workflow_with_stub(context: Context) -> None:
    svc = MagicMock()
    svc.get.return_value = MagicMock(actor_name="openai/gpt-4")
    svc.get_messages.return_value = []
    svc.append_message.return_value = MagicMock(spec=SessionMessage)
    svc.update_token_usage.return_value = None
    context.cb_workflow = SessionWorkflow(session_service=svc, provider_registry=None)


@given("coverage boost session history without a system message")
def step_cb_history_no_system(context: Context) -> None:
    context.cb_no_system_history = [
        SessionMessage(
            message_id="01KJ1N8YDN05N29P5RZV4SPDW0",
            role=MessageRole.USER,
            content="Hello",
            sequence=1,
            timestamp="2026-01-01T00:00:00Z",
        ),
    ]


@when("coverage boost _build_lc_messages_from_history is called with a prompt")
def step_cb_call_build_lc(context: Context) -> None:
    context.cb_built_messages = context.cb_workflow._build_lc_messages_from_history(
        context.cb_no_system_history, "test prompt"
    )


@then(
    "coverage boost the first message should be a SystemMessage with the session system prompt"
)
def step_cb_first_is_system(context: Context) -> None:
    from langchain_core.messages import SystemMessage

    assert len(context.cb_built_messages) > 0, "Expected at least one message"
    first = context.cb_built_messages[0]
    assert isinstance(first, SystemMessage), (
        f"Expected SystemMessage, got {type(first)}"
    )
    assert "CleverAgents orchestrator" in first.content, (
        f"Expected system prompt content, got {first.content}"
    )


# ====================================================================
# _MinimalStubLLM
# ====================================================================


@given("coverage boost a _MinimalStubLLM instance")
def step_cb_minimal_stub(context: Context) -> None:
    context.cb_stub = _MinimalStubLLM()


@when("coverage boost invoke on the stub is called")
def step_cb_stub_invoke(context: Context) -> None:
    context.cb_stub_response = context.cb_stub.invoke([])


@then('coverage boost the stub response content should be "(no LLM configured)"')
def step_cb_stub_content(context: Context) -> None:
    assert context.cb_stub_response.content == "(no LLM configured)"


@then("coverage boost the stub response should have empty tool_calls")
def step_cb_stub_empty_tool_calls(context: Context) -> None:
    assert context.cb_stub_response.tool_calls == []


@when("coverage boost stream on the stub is called")
def step_cb_stub_stream(context: Context) -> None:
    context.cb_stub_stream_chunks = list(context.cb_stub.stream([]))


@then('coverage boost it should yield a chunk with content "(no LLM configured)"')
def step_cb_stub_chunk_content(context: Context) -> None:
    assert len(context.cb_stub_stream_chunks) >= 1, "Expected at least one chunk"
    assert context.cb_stub_stream_chunks[0].content == "(no LLM configured)"
