import json
from unittest.mock import AsyncMock

import pytest

from control_backend.agents.bdi import (
    BDIBeliefCollectorAgent,
)
from control_backend.core.agent_system import InternalMessage
from control_backend.core.config import settings
from control_backend.schemas.belief_message import Belief


@pytest.fixture
def agent():
    agent = BDIBeliefCollectorAgent("belief_collector_agent")
    return agent


def make_msg(body: dict, sender: str = "sender"):
    return InternalMessage(to="collector", sender=sender, body=json.dumps(body))


@pytest.mark.asyncio
async def test_handle_message_routes_belief_text(agent, mocker):
    """
    Test that when a message is received, _handle_belief_text is called with that message.
    """
    payload = {"type": "belief_extraction_text", "beliefs": {"user_said": [["hi"]]}}
    spy = mocker.patch.object(agent, "_handle_belief_text", new_callable=AsyncMock)

    await agent.handle_message(make_msg(payload))

    spy.assert_awaited_once_with(payload, "sender")


@pytest.mark.asyncio
async def test_handle_message_routes_emotion(agent, mocker):
    payload = {"type": "emotion_extraction_text"}
    spy = mocker.patch.object(agent, "_handle_emo_text", new_callable=AsyncMock)

    await agent.handle_message(make_msg(payload))

    spy.assert_awaited_once_with(payload, "sender")


@pytest.mark.asyncio
async def test_handle_message_bad_json(agent, mocker):
    agent._handle_belief_text = AsyncMock()
    bad_msg = InternalMessage(to="collector", sender="sender", body="not json")

    await agent.handle_message(bad_msg)

    agent._handle_belief_text.assert_not_awaited()


@pytest.mark.asyncio
async def test_handle_belief_text_sends_when_beliefs_exist(agent, mocker):
    payload = {"type": "belief_extraction_text", "beliefs": {"user_said": ["hello"]}}
    spy = mocker.patch.object(agent, "_send_beliefs_to_bdi", new_callable=AsyncMock)
    expected = [Belief(name="user_said", arguments=["hello"])]

    await agent._handle_belief_text(payload, "origin")

    spy.assert_awaited_once_with(expected, origin="origin")


@pytest.mark.asyncio
async def test_handle_belief_text_no_send_when_empty(agent, mocker):
    payload = {"type": "belief_extraction_text", "beliefs": {}}
    spy = mocker.patch.object(agent, "_send_beliefs_to_bdi", new_callable=AsyncMock)

    await agent._handle_belief_text(payload, "origin")

    spy.assert_not_awaited()


@pytest.mark.asyncio
async def test_send_beliefs_to_bdi(agent):
    agent.send = AsyncMock()
    beliefs = [Belief(name="user_said", arguments=["hello", "world"])]

    await agent._send_beliefs_to_bdi(beliefs, origin="origin")

    agent.send.assert_awaited_once()
    sent: InternalMessage = agent.send.call_args.args[0]
    assert sent.to == settings.agent_settings.bdi_core_name
    assert sent.thread == "beliefs"
    assert json.loads(sent.body)["beliefs"] == [belief.model_dump() for belief in beliefs]


@pytest.mark.asyncio
async def test_setup_executes(agent):
    """Covers setup and asserts the agent has a name."""
    await agent.setup()
    assert agent.name == "belief_collector_agent"  # simple property assertion


@pytest.mark.asyncio
async def test_handle_message_unrecognized_type_executes(agent):
    """Covers the else branch for unrecognized message type."""
    payload = {"type": "unknown_type"}
    msg = make_msg(payload, sender="tester")
    # Wrap send to ensure nothing is sent
    agent.send = AsyncMock()
    await agent.handle_message(msg)
    # Assert no messages were sent
    agent.send.assert_not_awaited()


@pytest.mark.asyncio
async def test_handle_emo_text_executes(agent):
    """Covers the _handle_emo_text method."""
    # The method does nothing, but we can assert it returns None
    result = await agent._handle_emo_text({}, "origin")
    assert result is None


@pytest.mark.asyncio
async def test_send_beliefs_to_bdi_empty_executes(agent):
    """Covers early return when beliefs are empty."""
    agent.send = AsyncMock()
    await agent._send_beliefs_to_bdi({})
    # Assert that nothing was sent
    agent.send.assert_not_awaited()


@pytest.mark.asyncio
async def test_handle_belief_text_invalid_returns_none(agent, mocker):
    payload = {"type": "belief_extraction_text", "beliefs": {"user_said": "invalid-argument"}}

    result = await agent._handle_belief_text(payload, "origin")

    # The method itself returns None
    assert result is None
