"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""

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")
    agent.send = AsyncMock()
    agent.bdi_agent = MagicMock()
    return agent


@pytest.fixture(autouse=True)
def mock_experiment_logger():
    with patch("control_backend.agents.bdi.bdi_core_agent.experiment_logger") as logger:
        yield logger


@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_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.text_belief_extractor_name,
        body=BeliefMessage(create=beliefs).model_dump_json(),
        thread="beliefs",
    )

    await agent.handle_message(msg)

    # Check for the specific call we expect among all calls
    # bdi_agent.call is called multiple times (for transition_phase, check_triggers)
    # We want to confirm the belief addition call exists
    found_call = False
    for call in agent.bdi_agent.call.call_args_list:
        args = call.args
        if (
            args[0] == agentspeak.Trigger.addition
            and args[1] == agentspeak.GoalType.belief
            and args[2].functor == "user_said"
            and args[2].args[0].functor == "Hello"
        ):
            found_call = True
            break

    assert found_call, "Expected belief addition call not found in bdi_agent.call history"


@pytest.mark.asyncio
async def test_handle_delete_belief_message(agent, mock_settings):
    """Test that incoming beliefs to be deleted are removed from the BDI agent"""
    beliefs = [Belief(name="user_said", arguments=["Hello"])]

    msg = InternalMessage(
        to="bdi_agent",
        sender=mock_settings.agent_settings.text_belief_extractor_name,
        body=BeliefMessage(delete=beliefs).model_dump_json(),
        thread="beliefs",
    )
    await agent.handle_message(msg)

    found_call = False
    for call in agent.bdi_agent.call.call_args_list:
        args = call.args
        if (
            args[0] == agentspeak.Trigger.removal
            and args[1] == agentspeak.GoalType.belief
            and args[2].functor == "user_said"
            and args[2].args[0].functor == "Hello"
        ):
            found_call = True
            break

    assert found_call


@pytest.mark.asyncio
async def test_incorrect_belief_message(agent, mock_settings):
    """Test that incorrect message format triggers an exception."""
    msg = InternalMessage(
        to="bdi_agent",
        sender=mock_settings.agent_settings.text_belief_extractor_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"]
    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", "")


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"])
    belief_changes = BeliefMessage(replace=[belief])
    agent._apply_belief_changes(belief_changes)

    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_belief_changes(BeliefMessage())
    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

    call_args = agent.bdi_agent.call.call_args.args
    trigger = call_args[0]
    goaltype = call_args[1]
    literal = call_args[2]

    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"])
    belief_changes = BeliefMessage(replace=[belief])
    agent._apply_belief_changes(belief_changes)

    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


@pytest.mark.asyncio
async def test_handle_new_program(agent):
    agent._load_asl = AsyncMock()
    agent.add_behavior = MagicMock()
    # Mock existing loop task so it can be cancelled
    mock_task = MagicMock()
    mock_task.cancel = MagicMock()
    agent._bdi_loop_task = mock_task

    def close_coro(coro):
        coro.close()
        return MagicMock()

    agent.add_behavior = MagicMock(side_effect=close_coro)

    msg = InternalMessage(to="bdi_agent", thread="new_program", body="path/to/asl.asl")

    await agent.handle_message(msg)

    mock_task.cancel.assert_called_once()
    agent._load_asl.assert_awaited_once_with("path/to/asl.asl")
    agent.add_behavior.assert_called()


@pytest.mark.asyncio
async def test_handle_user_interrupts(agent, mock_settings):
    mock_settings.agent_settings.user_interrupt_name = "user_interrupt_agent"
    # force_phase_transition
    agent._set_goal = MagicMock()
    msg = InternalMessage(
        to="bdi_agent",
        sender=mock_settings.agent_settings.user_interrupt_name,
        thread="force_phase_transition",
        body="",
    )
    await agent.handle_message(msg)
    agent._set_goal.assert_called_with("transition_phase")

    # force_trigger
    agent._force_trigger = MagicMock()
    msg.thread = "force_trigger"
    msg.body = "trigger_x"
    await agent.handle_message(msg)
    agent._force_trigger.assert_called_with("trigger_x")

    # force_norm
    agent._force_norm = MagicMock()
    msg.thread = "force_norm"
    msg.body = "norm_y"
    await agent.handle_message(msg)
    agent._force_norm.assert_called_with("norm_y")

    # force_next_phase
    agent._force_next_phase = MagicMock()
    msg.thread = "force_next_phase"
    msg.body = ""
    await agent.handle_message(msg)
    agent._force_next_phase.assert_called_once()

    # unknown interrupt
    agent.logger = MagicMock()
    msg.thread = "unknown_thing"
    await agent.handle_message(msg)
    agent.logger.warning.assert_called()


@pytest.mark.asyncio
async def test_custom_action_reply_with_goal(agent):
    agent._send_to_llm = MagicMock(side_effect=agent.send)
    agent._add_custom_actions()
    action_fn = agent.actions.actions[(".reply_with_goal", 3)]

    mock_term = MagicMock(args=["msg", "norms", "goal"])
    gen = action_fn(agent, mock_term, MagicMock())
    next(gen)
    agent._send_to_llm.assert_called_with("msg", "norms", "goal")


@pytest.mark.asyncio
async def test_custom_action_notify_norms(agent):
    agent._add_custom_actions()
    action_fn = agent.actions.actions[(".notify_norms", 1)]

    mock_term = MagicMock(args=["norms_list"])
    gen = action_fn(agent, mock_term, MagicMock())
    next(gen)

    agent.send.assert_called()
    msg = agent.send.call_args[0][0]
    assert msg.thread == "active_norms_update"
    assert msg.body == "norms_list"


@pytest.mark.asyncio
async def test_custom_action_say(agent):
    agent._add_custom_actions()
    action_fn = agent.actions.actions[(".say", 1)]

    mock_term = MagicMock(args=["hello"])
    gen = action_fn(agent, mock_term, MagicMock())
    next(gen)

    assert agent.send.call_count == 2
    msgs = [c[0][0] for c in agent.send.call_args_list]
    assert any(m.to == settings.agent_settings.robot_speech_name for m in msgs)
    assert any(
        m.to == settings.agent_settings.llm_name and m.thread == "assistant_message" for m in msgs
    )


@pytest.mark.asyncio
async def test_custom_action_gesture(agent):
    agent._add_custom_actions()
    # Test single
    action_fn = agent.actions.actions[(".gesture", 2)]
    mock_term = MagicMock(args=["single", "wave"])
    gen = action_fn(agent, mock_term, MagicMock())
    next(gen)
    msg = agent.send.call_args[0][0]
    assert "actuate/gesture/single" in msg.body

    # Test tag
    mock_term.args = ["tag", "happy"]
    gen = action_fn(agent, mock_term, MagicMock())
    next(gen)
    msg = agent.send.call_args[0][0]
    assert "actuate/gesture/tag" in msg.body


@pytest.mark.asyncio
async def test_custom_action_notify_user_said(agent):
    agent._add_custom_actions()
    action_fn = agent.actions.actions[(".notify_user_said", 1)]
    mock_term = MagicMock(args=["hello"])
    gen = action_fn(agent, mock_term, MagicMock())
    next(gen)
    msg = agent.send.call_args[0][0]
    assert msg.to == settings.agent_settings.llm_name
    assert msg.thread == "user_message"


@pytest.mark.asyncio
async def test_custom_action_notify_trigger_start_end(agent):
    agent._add_custom_actions()
    # Start
    action_fn = agent.actions.actions[(".notify_trigger_start", 1)]
    gen = action_fn(agent, MagicMock(args=["t1"]), MagicMock())
    next(gen)
    assert agent.send.call_args[0][0].thread == "trigger_start"

    # End
    action_fn = agent.actions.actions[(".notify_trigger_end", 1)]
    gen = action_fn(agent, MagicMock(args=["t1"]), MagicMock())
    next(gen)
    assert agent.send.call_args[0][0].thread == "trigger_end"


@pytest.mark.asyncio
async def test_custom_action_notify_goal_start(agent):
    agent._add_custom_actions()
    action_fn = agent.actions.actions[(".notify_goal_start", 1)]
    gen = action_fn(agent, MagicMock(args=["g1"]), MagicMock())
    next(gen)
    assert agent.send.call_args[0][0].thread == "goal_start"


@pytest.mark.asyncio
async def test_custom_action_notify_transition_phase(agent):
    agent._add_custom_actions()
    action_fn = agent.actions.actions[(".notify_transition_phase", 2)]
    gen = action_fn(agent, MagicMock(args=["old", "new"]), MagicMock())
    next(gen)
    msg = agent.send.call_args[0][0]
    assert msg.thread == "transition_phase"
    assert "old" in msg.body and "new" in msg.body


def test_remove_belief_no_args(agent):
    agent._wake_bdi_loop = MagicMock()
    agent.bdi_agent.call.return_value = True
    agent._remove_belief("fact", None)
    assert agent.bdi_agent.call.called


def test_set_goal_with_args(agent):
    agent._wake_bdi_loop = MagicMock()
    agent._set_goal("goal", ["arg1", "arg2"])
    assert agent.bdi_agent.call.called


def test_format_belief_string():
    assert BDICoreAgent.format_belief_string("b") == "b"
    assert BDICoreAgent.format_belief_string("b", ["a1", "a2"]) == "b(a1,a2)"


def test_force_norm(agent):
    agent._add_belief = MagicMock()
    agent._force_norm("be_polite")
    agent._add_belief.assert_called_with("force_be_polite")


def test_force_trigger(agent):
    agent._set_goal = MagicMock()
    agent._force_trigger("trig")
    agent._set_goal.assert_called_with("trig")


def test_force_next_phase(agent):
    agent._set_goal = MagicMock()
    agent._force_next_phase()
    agent._set_goal.assert_called_with("force_transition_phase")
