import json
import logging
from unittest.mock import AsyncMock, MagicMock, call

import pytest

from control_backend.agents.bdi.behaviours.belief_setter import BeliefSetterBehaviour

# Define a constant for the collector agent name to use in tests
COLLECTOR_AGENT_NAME = "belief_collector"
COLLECTOR_AGENT_JID = f"{COLLECTOR_AGENT_NAME}@test"


@pytest.fixture
def mock_agent(mocker):
    """Fixture to create a mock BDIAgent."""
    agent = MagicMock()
    agent.bdi = MagicMock()
    agent.jid = "bdi_agent@test"
    return agent


@pytest.fixture
def belief_setter(mock_agent, mocker):
    """Fixture to create an instance of BeliefSetterBehaviour with a mocked agent."""
    # Patch the settings to use a predictable agent name
    mocker.patch(
        "control_backend.agents.bdi.behaviours.belief_setter.settings.agent_settings.belief_collector_agent_name",
        COLLECTOR_AGENT_NAME,
    )

    setter = BeliefSetterBehaviour()
    setter.agent = mock_agent
    # Mock the receive method, we will control its return value in each test
    setter.receive = AsyncMock()
    return setter


def create_mock_message(sender_node: str, body: str, thread: str) -> MagicMock:
    """Helper function to create a configured mock message."""
    msg = MagicMock()
    msg.sender.node = sender_node  # MagicMock automatically creates nested mocks
    msg.body = body
    msg.thread = thread
    return msg


@pytest.mark.asyncio
async def test_run_no_message_received(belief_setter, mocker):
    """
    Test that when no message is received, _process_message is not called.
    """
    # Arrange
    belief_setter.receive.return_value = None
    mocker.patch.object(belief_setter, "_process_message")

    # Act
    await belief_setter.run()

    # Assert
    belief_setter._process_message.assert_not_called()


@pytest.mark.asyncio
async def test_run_message_received(belief_setter, mocker):
    """
    Test that when a message is received, _process_message is called.
    """
    # Arrange
    msg = MagicMock()
    belief_setter.receive.return_value = msg
    mocker.patch.object(belief_setter, "_process_message")

    # Act
    await belief_setter.run()

    # Assert
    belief_setter._process_message.assert_called_once_with(msg)


def test_process_message_from_belief_collector(belief_setter, mocker):
    """
    Test processing a message from the correct belief collector agent.
    """
    # Arrange
    msg = create_mock_message(sender_node=COLLECTOR_AGENT_NAME, body="", thread="")
    mock_process_belief = mocker.patch.object(belief_setter, "_process_belief_message")

    # Act
    belief_setter._process_message(msg)

    # Assert
    mock_process_belief.assert_called_once_with(msg)


def test_process_message_from_other_agent(belief_setter, mocker):
    """
    Test that messages from other agents are ignored.
    """
    # Arrange
    msg = create_mock_message(sender_node="other_agent", body="", thread="")
    mock_process_belief = mocker.patch.object(belief_setter, "_process_belief_message")

    # Act
    belief_setter._process_message(msg)

    # Assert
    mock_process_belief.assert_not_called()


def test_process_belief_message_valid_json(belief_setter, mocker):
    """
    Test processing a valid belief message with correct thread and JSON body.
    """
    # Arrange
    beliefs_payload = {"is_hot": ["kitchen"], "is_clean": ["kitchen", "bathroom"]}
    msg = create_mock_message(
        sender_node=COLLECTOR_AGENT_JID, body=json.dumps(beliefs_payload), thread="beliefs"
    )
    mock_set_beliefs = mocker.patch.object(belief_setter, "_set_beliefs")

    # Act
    belief_setter._process_belief_message(msg)

    # Assert
    mock_set_beliefs.assert_called_once_with(beliefs_payload)


def test_process_belief_message_invalid_json(belief_setter, mocker, caplog):
    """
    Test that a message with invalid JSON is handled gracefully and an error is logged.
    """
    # Arrange
    msg = create_mock_message(
        sender_node=COLLECTOR_AGENT_JID, body="this is not a json string", thread="beliefs"
    )
    mock_set_beliefs = mocker.patch.object(belief_setter, "_set_beliefs")

    # Act
    with caplog.at_level(logging.ERROR):
        belief_setter._process_belief_message(msg)

    # Assert
    mock_set_beliefs.assert_not_called()
    assert "Could not decode beliefs into JSON format" in caplog.text


def test_process_belief_message_wrong_thread(belief_setter, mocker):
    """
    Test that a message with an incorrect thread is ignored.
    """
    # Arrange
    msg = create_mock_message(
        sender_node=COLLECTOR_AGENT_JID, body='{"some": "data"}', thread="not_beliefs"
    )
    mock_set_beliefs = mocker.patch.object(belief_setter, "_set_beliefs")

    # Act
    belief_setter._process_belief_message(msg)

    # Assert
    mock_set_beliefs.assert_not_called()


def test_process_belief_message_empty_body(belief_setter, mocker):
    """
    Test that a message with an empty body is ignored.
    """
    # Arrange
    msg = create_mock_message(sender_node=COLLECTOR_AGENT_JID, body="", thread="beliefs")
    mock_set_beliefs = mocker.patch.object(belief_setter, "_set_beliefs")

    # Act
    belief_setter._process_belief_message(msg)

    # Assert
    mock_set_beliefs.assert_not_called()


def test_set_beliefs_success(belief_setter, mock_agent, caplog):
    """
    Test that beliefs are correctly set on the agent's BDI.
    """
    # Arrange
    beliefs_to_set = {
        "is_hot": ["kitchen"],
        "door_opened": ["front_door", "back_door"],
    }

    # Act
    with caplog.at_level(logging.INFO):
        belief_setter._set_beliefs(beliefs_to_set)

    # Assert
    expected_calls = [
        call("is_hot", "kitchen"),
        call("door_opened", "front_door", "back_door"),
    ]
    mock_agent.bdi.set_belief.assert_has_calls(expected_calls, any_order=True)
    assert mock_agent.bdi.set_belief.call_count == 2

    # Check logs
    assert "Set belief is_hot with arguments ['kitchen']" in caplog.text
    assert "Set belief door_opened with arguments ['front_door', 'back_door']" in caplog.text


# def test_responded_unset(belief_setter, mock_agent):
#     # Arrange
#     new_beliefs = {"user_said": ["message"]}
#
#     # Act
#     belief_setter._set_beliefs(new_beliefs)
#
#     # Assert
#     mock_agent.bdi.set_belief.assert_has_calls([call("user_said", "message")])
#     mock_agent.bdi.remove_belief.assert_has_calls([call("responded")])

# def test_set_beliefs_bdi_not_initialized(belief_setter, mock_agent, caplog):
#     """
#     Test that a warning is logged if the agent's BDI is not initialized.
#     """
#     # Arrange
#     mock_agent.bdi = None  # Simulate BDI not being ready
#     beliefs_to_set = {"is_hot": ["kitchen"]}
#
#     # Act
#     with caplog.at_level(logging.WARNING):
#         belief_setter._set_beliefs(beliefs_to_set)
#
#     # Assert
#     assert "Cannot set beliefs, since agent's BDI is not yet initialized." in caplog.text
