"""Tests that the scheduler uses the Agent framework for citation detection.

TDD RED phase: these tests define the desired behavior where the scheduler
dispatches citation tasks through the Agent framework instead of directly
using CitationEngine.
"""
import inspect
import pytest
import uuid
from datetime import datetime, timezone
from unittest.mock import AsyncMock, MagicMock, patch, PropertyMock

from app.agent_framework.protocol import (
    TaskMessage,
    TaskResult,
    TaskStatus,
)


class TestSchedulerUsesAgentFramework:
    """Scheduler should dispatch citation tasks through the Agent framework."""

    @pytest.mark.asyncio
    async def test_scheduler_dispatches_via_agent_for_full_query(self):
        """check_and_execute_queries should use CitationDetectorAgent
        instead of directly calling CitationEngine."""
        # Setup mock agent
        mock_agent_instance = AsyncMock()
        mock_agent_instance.execute_query_compat = AsyncMock(return_value=[])

        with patch("app.workers.scheduler.AsyncSessionLocal") as mock_session_local, \
             patch("app.workers.scheduler._get_agent", return_value=mock_agent_instance), \
             patch("app.workers.scheduler.CitationEngine") as MockEngine:

            # Setup mock db session
            mock_db = AsyncMock()
            mock_session_local.return_value.__aenter__ = AsyncMock(return_value=mock_db)
            mock_session_local.return_value.__aexit__ = AsyncMock(return_value=False)

            # Setup mock query
            mock_query = MagicMock()
            mock_query.id = uuid.uuid4()
            mock_query.keyword = "test keyword"
            mock_query.status = "active"
            mock_query.next_query_at = datetime.now(timezone.utc)

            mock_result = MagicMock()
            mock_result.scalars.return_value.all.return_value = [mock_query]
            mock_db.execute = AsyncMock(return_value=mock_result)

            from app.workers.scheduler import QueryScheduler
            scheduler = QueryScheduler()
            await scheduler.check_and_execute_queries()

            # Agent's compat method should have been called
            mock_agent_instance.execute_query_compat.assert_called_once()
            # Direct engine should NOT have been used
            MockEngine.assert_not_called()

    @pytest.mark.asyncio
    async def test_scheduler_dispatches_via_agent_for_pending_tasks(self):
        """check_and_execute_pending_tasks should use CitationDetectorAgent
        instead of directly calling CitationEngine."""
        # Setup mock agent
        mock_agent_instance = AsyncMock()
        mock_agent_instance.execute_single_platform_compat = AsyncMock(
            return_value={"cited": False}
        )

        with patch("app.workers.scheduler.AsyncSessionLocal") as mock_session_local, \
             patch("app.workers.scheduler._get_agent", return_value=mock_agent_instance), \
             patch("app.workers.scheduler.CitationEngine") as MockEngine:

            # Setup mock db session
            mock_db = AsyncMock()
            mock_session_local.return_value.__aenter__ = AsyncMock(return_value=mock_db)
            mock_session_local.return_value.__aexit__ = AsyncMock(return_value=False)

            # Setup mock query task
            query_id = uuid.uuid4()
            mock_task = MagicMock()
            mock_task.id = uuid.uuid4()
            mock_task.query_id = query_id
            mock_task.platform = "kimi"
            mock_task.status = "pending"
            mock_task.scheduled_at = datetime.now(timezone.utc)

            mock_task_result = MagicMock()
            mock_task_result.scalars.return_value.all.return_value = [mock_task]

            # Setup mock query
            mock_query = MagicMock()
            mock_query.id = query_id
            mock_query.status = "active"
            mock_query.keyword = "test"
            mock_query.target_brand = "Brand"
            mock_query.brand_aliases = []

            mock_query_result = MagicMock()
            mock_query_result.scalar_one_or_none.return_value = mock_query

            # First call returns tasks, second returns query
            mock_db.execute = AsyncMock(
                side_effect=[mock_task_result, mock_query_result]
            )
            mock_db.commit = AsyncMock()

            from app.workers.scheduler import QueryScheduler
            scheduler = QueryScheduler()
            await scheduler.check_and_execute_pending_tasks()

            # Agent's compat method should have been called
            mock_agent_instance.execute_single_platform_compat.assert_called_once()
            # Direct engine should NOT have been used
            MockEngine.assert_not_called()

    @pytest.mark.asyncio
    async def test_scheduler_falls_back_to_engine_on_agent_failure(self):
        """If Agent framework fails, scheduler should fall back to direct engine."""
        # Agent raises exception
        mock_agent_instance = AsyncMock()
        mock_agent_instance.execute_query_compat = AsyncMock(
            side_effect=Exception("Agent framework unavailable")
        )

        with patch("app.workers.scheduler.AsyncSessionLocal") as mock_session_local, \
             patch("app.workers.scheduler._get_agent", return_value=mock_agent_instance), \
             patch("app.workers.scheduler.CitationEngine") as MockEngine:

            # Fallback engine works
            mock_engine_instance = AsyncMock()
            mock_engine_instance.execute_query = AsyncMock(return_value=[])
            MockEngine.return_value = mock_engine_instance

            # Setup mock db session
            mock_db = AsyncMock()
            mock_session_local.return_value.__aenter__ = AsyncMock(return_value=mock_db)
            mock_session_local.return_value.__aexit__ = AsyncMock(return_value=False)

            # Setup mock query
            mock_query = MagicMock()
            mock_query.id = uuid.uuid4()
            mock_query.keyword = "test keyword"
            mock_query.status = "active"
            mock_query.next_query_at = datetime.now(timezone.utc)

            mock_result = MagicMock()
            mock_result.scalars.return_value.all.return_value = [mock_query]
            mock_db.execute = AsyncMock(return_value=mock_result)

            from app.workers.scheduler import QueryScheduler
            scheduler = QueryScheduler()
            await scheduler.check_and_execute_queries()

            # Agent was tried first
            mock_agent_instance.execute_query_compat.assert_called_once()
            # Fallback engine was used
            MockEngine.assert_called_once()
            mock_engine_instance.execute_query.assert_called_once()

    @pytest.mark.asyncio
    async def test_citation_detector_agent_produces_same_interface_as_engine(self):
        """CitationDetectorAgent compat methods should have the same
        call signature and return type as CitationEngine methods."""
        from app.agent_framework.agents.citation_detector import CitationDetectorAgent
        from app.workers.citation_engine import CitationEngine

        engine_query_sig = inspect.signature(CitationEngine.execute_query)
        agent_compat_sig = inspect.signature(CitationDetectorAgent.execute_query_compat)

        # Same parameter names
        engine_params = list(engine_query_sig.parameters.keys())
        agent_params = list(agent_compat_sig.parameters.keys())
        assert engine_params == agent_params, (
            f"Parameter mismatch: engine={engine_params}, agent={agent_params}"
        )

        engine_single_sig = inspect.signature(CitationEngine.execute_single_platform)
        agent_single_sig = inspect.signature(
            CitationDetectorAgent.execute_single_platform_compat
        )

        engine_single_params = list(engine_single_sig.parameters.keys())
        agent_single_params = list(agent_single_sig.parameters.keys())
        assert engine_single_params == agent_single_params, (
            f"Parameter mismatch: engine={engine_single_params}, "
            f"agent={agent_single_params}"
        )

    @pytest.mark.asyncio
    async def test_scheduler_does_not_create_engine_in_init(self):
        """QueryScheduler should NOT create a CitationEngine instance
        in __init__. It should use the Agent framework instead."""
        with patch("app.workers.scheduler.CitationEngine") as MockEngine:
            from app.workers.scheduler import QueryScheduler
            scheduler = QueryScheduler()

            # Engine should NOT have been instantiated in __init__
            MockEngine.assert_not_called()


class TestBaseAgentDispatcherInjection:
    """BaseAgent should not create TaskDispatcher on every method call."""

    def test_base_agent_does_not_create_dispatcher_in_report_progress(self):
        """report_progress should use an injected dispatcher, not create a new one."""
        from app.agent_framework.base import BaseAgent

        # Check that report_progress doesn't import TaskDispatcher inline
        source = inspect.getsource(BaseAgent.report_progress)
        assert "TaskDispatcher(" not in source, (
            "report_progress should not instantiate TaskDispatcher directly. "
            "Use an injected or cached dispatcher instead."
        )

    def test_base_agent_does_not_create_dispatcher_in_execute_task(self):
        """_execute_task should use an injected dispatcher, not create a new one."""
        from app.agent_framework.base import BaseAgent

        source = inspect.getsource(BaseAgent._execute_task)
        assert "TaskDispatcher(" not in source, (
            "_execute_task should not instantiate TaskDispatcher directly. "
            "Use an injected or cached dispatcher instead."
        )
