import asyncio
import json
import time
from unittest.mock import AsyncMock, MagicMock, mock_open, patch

import agentspeak
import pytest

from control_backend.agents.bdi.bdi_core_agent import BDICoreAgent
from control_backend.core.agent_system import InternalMessage
from control_backend.core.config import settings
from control_backend.schemas.belief_message import Belief, BeliefMessage


@pytest.fixture
def mock_agentspeak_env():
    with patch("agentspeak.runtime.Environment") as mock_env:
        yield mock_env


@pytest.fixture
def agent():
    agent = BDICoreAgent("bdi_agent", "dummy.asl")
    agent.send = AsyncMock()
    agent.bdi_agent = MagicMock()
    return agent


@pytest.mark.asyncio
async def test_setup_loads_asl(mock_agentspeak_env, agent):
    # Mock file opening
    with patch("builtins.open", mock_open(read_data="+initial_goal.")):
        await agent.setup()

        # Check if environment tried to build agent
        mock_agentspeak_env.return_value.build_agent.assert_called()


@pytest.mark.asyncio
async def test_setup_no_asl(mock_agentspeak_env, agent):
    with patch("builtins.open", side_effect=FileNotFoundError):
        await agent.setup()

        mock_agentspeak_env.return_value.build_agent.assert_not_called()


@pytest.mark.asyncio
async def test_handle_belief_collector_message(agent, mock_settings):
    """Test that incoming beliefs are added to the BDI agent"""
    beliefs = [Belief(name="user_said", arguments=["Hello"])]
    msg = InternalMessage(
        to="bdi_agent",
        sender=mock_settings.agent_settings.bdi_belief_collector_name,
        body=BeliefMessage(beliefs=beliefs).model_dump_json(),
        thread="beliefs",
    )

    await agent.handle_message(msg)

    # Expect bdi_agent.call to be triggered to add belief
    args = agent.bdi_agent.call.call_args.args
    assert args[0] == agentspeak.Trigger.addition
    assert args[1] == agentspeak.GoalType.belief
    assert args[2] == agentspeak.Literal("user_said", (agentspeak.Literal("Hello"),))


@pytest.mark.asyncio
async def test_incorrect_belief_collector_message(agent, mock_settings):
    """Test that incorrect message format triggers an exception."""
    msg = InternalMessage(
        to="bdi_agent",
        sender=mock_settings.agent_settings.bdi_belief_collector_name,
        body=json.dumps({"bad_format": "bad_format"}),
        thread="beliefs",
    )

    await agent.handle_message(msg)

    agent.bdi_agent.call.assert_not_called()  # did not set belief


@pytest.mark.asyncio
async def test_handle_llm_response(agent):
    """Test that LLM responses are forwarded to the Robot Speech Agent"""
    msg = InternalMessage(
        to="bdi_agent", sender=settings.agent_settings.llm_name, body="This is the LLM reply"
    )

    await agent.handle_message(msg)

    # Verify forward
    assert agent.send.called
    sent_msg = agent.send.call_args[0][0]
    assert sent_msg.to == settings.agent_settings.robot_speech_name
    assert "This is the LLM reply" in sent_msg.body


@pytest.mark.asyncio
async def test_custom_actions(agent):
    agent._send_to_llm = MagicMock(side_effect=agent.send)  # Mock specific method

    # Initialize actions manually since we didn't call setup with real file
    agent._add_custom_actions()

    # Find the action
    action_fn = None
    for (functor, _), fn in agent.actions.actions.items():
        if functor == ".reply":
            action_fn = fn
            break

    assert action_fn is not None

    # Invoke action
    mock_term = MagicMock()
    mock_term.args = ["Hello", "Norm", "Goal"]
    mock_intention = MagicMock()

    # Run generator
    gen = action_fn(agent, mock_term, mock_intention)
    next(gen)  # Execute

    agent._send_to_llm.assert_called_with("Hello", "Norm", "Goal")


def test_add_belief_sets_event(agent):
    """Test that a belief triggers wake event and call()"""
    agent._wake_bdi_loop = MagicMock()

    belief = Belief(name="test_belief", arguments=["a", "b"])
    agent._apply_beliefs([belief])

    assert agent.bdi_agent.call.called
    agent._wake_bdi_loop.set.assert_called()


def test_apply_beliefs_empty_returns(agent):
    """Line: if not beliefs: return"""
    agent._wake_bdi_loop = MagicMock()
    agent._apply_beliefs([])
    agent.bdi_agent.call.assert_not_called()
    agent._wake_bdi_loop.set.assert_not_called()


def test_remove_belief_success_wakes_loop(agent):
    """Line: if result: wake set"""
    agent._wake_bdi_loop = MagicMock()
    agent.bdi_agent.call.return_value = True

    agent._remove_belief("remove_me", ["x"])

    assert agent.bdi_agent.call.called
    trigger, goaltype, literal, *_ = agent.bdi_agent.call.call_args.args

    assert trigger == agentspeak.Trigger.removal
    assert goaltype == agentspeak.GoalType.belief
    assert literal.functor == "remove_me"
    assert literal.args[0].functor == "x"

    agent._wake_bdi_loop.set.assert_called()


def test_remove_belief_failure_does_not_wake(agent):
    """Line: else result is False"""
    agent._wake_bdi_loop = MagicMock()
    agent.bdi_agent.call.return_value = False

    agent._remove_belief("not_there", ["y"])

    assert agent.bdi_agent.call.called  # removal was attempted
    agent._wake_bdi_loop.set.assert_not_called()


def test_remove_all_with_name_wakes_loop(agent):
    """Cover _remove_all_with_name() removed counter + wake"""
    agent._wake_bdi_loop = MagicMock()

    fake_literal = agentspeak.Literal("delete_me", (agentspeak.Literal("arg1"),))
    fake_key = ("delete_me", 1)
    agent.bdi_agent.beliefs = {fake_key: {fake_literal}}

    agent._remove_all_with_name("delete_me")

    assert agent.bdi_agent.call.called
    agent._wake_bdi_loop.set.assert_called()


@pytest.mark.asyncio
async def test_bdi_step_true_branch_hits_line_67(agent):
    """Force step() to return True once so line 67 is actually executed"""
    # counter that isn't tied to MagicMock.call_count ordering
    counter = {"i": 0}

    def fake_step():
        counter["i"] += 1
        return counter["i"] == 1  # True only first time

    # Important: wrap fake_step into another mock so `.called` still exists
    agent.bdi_agent.step = MagicMock(side_effect=fake_step)
    agent.bdi_agent.shortest_deadline = MagicMock(return_value=None)

    agent._running = True
    agent._wake_bdi_loop = asyncio.Event()
    agent._wake_bdi_loop.set()

    task = asyncio.create_task(agent._bdi_loop())
    await asyncio.sleep(0.01)
    task.cancel()
    try:
        await task
    except asyncio.CancelledError:
        pass

    assert agent.bdi_agent.step.called
    assert counter["i"] >= 1  # proves True branch ran


def test_replace_belief_calls_remove_all(agent):
    """Cover: if belief.replace: self._remove_all_with_name()"""
    agent._remove_all_with_name = MagicMock()
    agent._wake_bdi_loop = MagicMock()

    belief = Belief(name="user_said", arguments=["Hello"], replace=True)
    agent._apply_beliefs([belief])

    agent._remove_all_with_name.assert_called_with("user_said")


@pytest.mark.asyncio
async def test_send_to_llm_creates_prompt_and_sends(agent):
    """Cover entire _send_to_llm() including message send and logger.info"""
    agent.bdi_agent = MagicMock()  # ensure mocked BDI does not interfere
    agent._wake_bdi_loop = MagicMock()

    await agent._send_to_llm("hello world", "n1\nn2", "g1")

    # send() was called
    assert agent.send.called
    sent_msg: InternalMessage = agent.send.call_args.args[0]

    # Message routing values correct
    assert sent_msg.to == settings.agent_settings.llm_name
    assert "hello world" in sent_msg.body

    # JSON contains split norms/goals
    body = json.loads(sent_msg.body)
    assert body["norms"] == ["n1", "n2"]
    assert body["goals"] == ["g1"]


@pytest.mark.asyncio
async def test_deadline_sleep_branch(agent):
    """Specifically assert the if deadline: sleep → maybe_more_work=True branch"""
    future_deadline = time.time() + 0.005
    agent.bdi_agent.step.return_value = False
    agent.bdi_agent.shortest_deadline.return_value = future_deadline

    start_time = time.time()
    agent._running = True
    agent._wake_bdi_loop = asyncio.Event()
    agent._wake_bdi_loop.set()

    task = asyncio.create_task(agent._bdi_loop())
    await asyncio.sleep(0.01)
    task.cancel()

    duration = time.time() - start_time
    assert duration >= 0.004  # loop slept until deadline
