#
# Copyright (c) 2024-2026, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#

import asyncio
import unittest

from pipecat.frames.frames import (
    BotSpeakingFrame,
    FunctionCallResultFrame,
    FunctionCallsStartedFrame,
    UserSpeakingFrame,
    UserStartedSpeakingFrame,
)
from pipecat.turns.user_idle_controller import UserIdleController
from pipecat.utils.asyncio.task_manager import TaskManager, TaskManagerParams

USER_IDLE_TIMEOUT = 0.2


class TestUserIdleController(unittest.IsolatedAsyncioTestCase):
    async def asyncSetUp(self):
        self.task_manager = TaskManager()
        self.task_manager.setup(TaskManagerParams(loop=asyncio.get_running_loop()))

    async def test_basic_idle_detection(self):
        """Test that idle event is triggered after timeout when no activity."""
        controller = UserIdleController(user_idle_timeout=USER_IDLE_TIMEOUT)
        await controller.setup(self.task_manager)

        idle_triggered = False

        @controller.event_handler("on_user_turn_idle")
        async def on_user_turn_idle(controller):
            nonlocal idle_triggered
            idle_triggered = True

        # Start conversation
        await controller.process_frame(UserStartedSpeakingFrame())

        # Wait for idle timeout
        await asyncio.sleep(USER_IDLE_TIMEOUT + 0.1)

        self.assertTrue(idle_triggered)

        await controller.cleanup()

    async def test_user_speaking_resets_idle_timer(self):
        """Test that continuous UserSpeakingFrame frames reset the idle timer."""
        controller = UserIdleController(user_idle_timeout=USER_IDLE_TIMEOUT)
        await controller.setup(self.task_manager)

        idle_triggered = False

        @controller.event_handler("on_user_turn_idle")
        async def on_user_turn_idle(controller):
            nonlocal idle_triggered
            idle_triggered = True

        # Start conversation
        await controller.process_frame(UserStartedSpeakingFrame())

        # Send UserSpeakingFrame continuously to reset timer
        for _ in range(5):
            await asyncio.sleep(USER_IDLE_TIMEOUT * 0.5)  # 50% of timeout period
            await controller.process_frame(UserSpeakingFrame())

        self.assertFalse(idle_triggered)

        await controller.cleanup()

    async def test_bot_speaking_resets_idle_timer(self):
        """Test that BotSpeakingFrame frames reset the idle timer."""
        controller = UserIdleController(user_idle_timeout=USER_IDLE_TIMEOUT)
        await controller.setup(self.task_manager)

        idle_triggered = False

        @controller.event_handler("on_user_turn_idle")
        async def on_user_turn_idle(controller):
            nonlocal idle_triggered
            idle_triggered = True

        # Start conversation
        await controller.process_frame(UserStartedSpeakingFrame())

        # Bot speaking should reset timer
        for _ in range(5):
            await asyncio.sleep(USER_IDLE_TIMEOUT * 0.6)  # 60% of timeout
            await controller.process_frame(BotSpeakingFrame())

        self.assertFalse(idle_triggered)

        await controller.cleanup()

    async def test_function_call_prevents_idle(self):
        """Test that function calls in progress prevent idle event."""
        controller = UserIdleController(user_idle_timeout=USER_IDLE_TIMEOUT)
        await controller.setup(self.task_manager)

        idle_triggered = False

        @controller.event_handler("on_user_turn_idle")
        async def on_user_turn_idle(controller):
            nonlocal idle_triggered
            idle_triggered = True

        # Start conversation
        await controller.process_frame(UserStartedSpeakingFrame())

        # Start function call
        await controller.process_frame(FunctionCallsStartedFrame(function_calls=[]))

        # Wait longer than idle timeout
        await asyncio.sleep(USER_IDLE_TIMEOUT + 0.1)

        # Should not trigger idle because function call is in progress
        self.assertFalse(idle_triggered)

        # Complete function call
        await controller.process_frame(
            FunctionCallResultFrame(
                function_name="test",
                tool_call_id="123",
                arguments={},
                result=None,
                run_llm=False,
            )
        )

        # Now idle should trigger
        await asyncio.sleep(USER_IDLE_TIMEOUT + 0.1)
        self.assertTrue(idle_triggered)

        await controller.cleanup()

    async def test_no_idle_before_conversation_starts(self):
        """Test that idle monitoring doesn't start before first conversation activity."""
        controller = UserIdleController(user_idle_timeout=USER_IDLE_TIMEOUT)
        await controller.setup(self.task_manager)

        idle_triggered = False

        @controller.event_handler("on_user_turn_idle")
        async def on_user_turn_idle(controller):
            nonlocal idle_triggered
            idle_triggered = True

        # Wait without starting conversation
        await asyncio.sleep(USER_IDLE_TIMEOUT + 0.1)

        self.assertFalse(idle_triggered)

        await controller.cleanup()

    async def test_idle_starts_with_bot_speaking(self):
        """Test that monitoring starts with BotSpeakingFrame, not just user speech."""
        controller = UserIdleController(user_idle_timeout=USER_IDLE_TIMEOUT)
        await controller.setup(self.task_manager)

        idle_triggered = False

        @controller.event_handler("on_user_turn_idle")
        async def on_user_turn_idle(controller):
            nonlocal idle_triggered
            idle_triggered = True

        # Start conversation with bot speaking
        await controller.process_frame(BotSpeakingFrame())

        # Wait for idle timeout
        await asyncio.sleep(USER_IDLE_TIMEOUT + 0.1)

        self.assertTrue(idle_triggered)

        await controller.cleanup()

    async def test_multiple_idle_events(self):
        """Test that idle event can trigger multiple times."""
        controller = UserIdleController(user_idle_timeout=USER_IDLE_TIMEOUT)
        await controller.setup(self.task_manager)

        idle_count = 0

        @controller.event_handler("on_user_turn_idle")
        async def on_user_turn_idle(controller):
            nonlocal idle_count
            idle_count += 1

        # Start conversation
        await controller.process_frame(UserStartedSpeakingFrame())

        # First idle
        await asyncio.sleep(USER_IDLE_TIMEOUT + 0.1)
        first_count = idle_count
        self.assertGreaterEqual(first_count, 1)

        # Second idle
        await asyncio.sleep(USER_IDLE_TIMEOUT + 0.1)
        second_count = idle_count
        self.assertGreater(second_count, first_count)

        # User activity resets timer
        await controller.process_frame(UserSpeakingFrame())

        # Give a moment for the timer to reset
        await asyncio.sleep(0.1)

        # Third idle
        await asyncio.sleep(USER_IDLE_TIMEOUT + 0.1)
        third_count = idle_count
        self.assertGreater(third_count, second_count)

        await controller.cleanup()
