"""
Integration tests for Slack Bot dispatcher.

Tests message dispatch flow with mocked Slack client and LLM,
verifying tool invocation, context management, and response formatting.
"""

import pytest
from unittest.mock import MagicMock, patch, call


class TestDispatcherBasicFlow:
    """Test core dispatch pipeline with mocked dependencies."""

    @patch("slack_bot.dispatcher.WebClient")
    @patch("slack_bot.dispatcher.GeminiLLM")
    @patch("slack_bot.dispatcher.ContextStorage")
    def test_simple_text_response(
        self, mock_storage_cls: MagicMock, mock_llm_cls: MagicMock, mock_client_cls: MagicMock
    ) -> None:
        """Dispatcher posts LLM text response to Slack channel."""
        from slack_bot.dispatcher import MessageDispatcher

        mock_client = MagicMock()
        mock_client_cls.return_value = mock_client

        mock_llm = MagicMock()
        mock_llm_cls.return_value = mock_llm
        mock_llm.generate_response.return_value = ("你好！有什么可以帮你的？", None)

        mock_storage = MagicMock()
        mock_storage_cls.return_value = mock_storage
        mock_storage.get_context.return_value = []

        dispatcher = MessageDispatcher()
        dispatcher.dispatch("你好", "C123", "U456")

        mock_client.chat_postMessage.assert_called_with(
            channel="C123",
            text="你好！有什么可以帮你的？",
        )

    @patch("slack_bot.dispatcher.WebClient")
    @patch("slack_bot.dispatcher.GeminiLLM")
    @patch("slack_bot.dispatcher.ContextStorage")
    def test_user_message_saved_to_context(
        self, mock_storage_cls: MagicMock, mock_llm_cls: MagicMock, mock_client_cls: MagicMock
    ) -> None:
        """User message is persisted to context storage."""
        from slack_bot.dispatcher import MessageDispatcher

        mock_llm = MagicMock()
        mock_llm_cls.return_value = mock_llm
        mock_llm.generate_response.return_value = ("OK", None)

        mock_storage = MagicMock()
        mock_storage_cls.return_value = mock_storage
        mock_storage.get_context.return_value = []

        mock_client_cls.return_value = MagicMock()

        dispatcher = MessageDispatcher()
        dispatcher.dispatch("记录午餐：鸡胸肉", "C456", "U789")

        mock_storage.add_message.assert_any_call("user", "记录午餐：鸡胸肉")

    @patch("slack_bot.dispatcher.WebClient")
    @patch("slack_bot.dispatcher.GeminiLLM")
    @patch("slack_bot.dispatcher.ContextStorage")
    def test_assistant_response_saved_to_context(
        self, mock_storage_cls: MagicMock, mock_llm_cls: MagicMock, mock_client_cls: MagicMock
    ) -> None:
        """Assistant response is persisted to context storage after dispatch."""
        from slack_bot.dispatcher import MessageDispatcher

        mock_llm = MagicMock()
        mock_llm_cls.return_value = mock_llm
        mock_llm.generate_response.return_value = ("已记录！", None)

        mock_storage = MagicMock()
        mock_storage_cls.return_value = mock_storage
        mock_storage.get_context.return_value = []

        mock_client_cls.return_value = MagicMock()

        dispatcher = MessageDispatcher()
        dispatcher.dispatch("记录午餐", "C789", "U111")

        # The assistant response should be saved
        saved_calls = [str(c) for c in mock_storage.add_message.call_args_list]
        assert any("assistant" in c for c in saved_calls)

    @patch("slack_bot.dispatcher.WebClient")
    @patch("slack_bot.dispatcher.GeminiLLM")
    @patch("slack_bot.dispatcher.ContextStorage")
    def test_tool_call_executed(
        self, mock_storage_cls: MagicMock, mock_llm_cls: MagicMock, mock_client_cls: MagicMock
    ) -> None:
        """When LLM returns a tool call, the tool function is executed."""
        from slack_bot.dispatcher import MessageDispatcher

        mock_llm = MagicMock()
        mock_llm_cls.return_value = mock_llm

        # Dispatcher expects {"name": ..., "args": ...} format
        tool_call = {
            "name": "get_daily_detailed_stats",
            "args": {"target_date": "2026-03-01"},
        }
        # Stage 1: return tool_call; Stage 2: return final text
        mock_llm.generate_response.side_effect = [
            ("", [tool_call]),
            ("昨天睡眠分数 82 分，步数 8000 步。", None),
        ]

        mock_storage = MagicMock()
        mock_storage_cls.return_value = mock_storage
        mock_storage.get_context.return_value = []

        mock_client_cls.return_value = MagicMock()

        with patch(
            "slack_bot.tools.registry.TOOL_FUNCTIONS",
            {"get_daily_detailed_stats": MagicMock(return_value="sleep_score: 82, steps: 8000")},
        ):
            dispatcher = MessageDispatcher()
            dispatcher.dispatch("昨天健康数据", "C001", "U001")

        # Tool was invoked (LLM log confirms it), and final response was generated
        mock_llm.generate_response.assert_called()
        assert mock_llm.generate_response.call_count == 2

    @patch("slack_bot.dispatcher.WebClient")
    @patch("slack_bot.dispatcher.GeminiLLM")
    @patch("slack_bot.dispatcher.ContextStorage")
    def test_empty_llm_response_fallback(
        self, mock_storage_cls: MagicMock, mock_llm_cls: MagicMock, mock_client_cls: MagicMock
    ) -> None:
        """Empty LLM response triggers a fallback message, not a crash."""
        from slack_bot.dispatcher import MessageDispatcher

        mock_llm = MagicMock()
        mock_llm_cls.return_value = mock_llm
        mock_llm.generate_response.return_value = ("", None)

        mock_storage = MagicMock()
        mock_storage_cls.return_value = mock_storage
        mock_storage.get_context.return_value = []

        mock_client = MagicMock()
        mock_client_cls.return_value = mock_client

        dispatcher = MessageDispatcher()
        # Should not raise
        dispatcher.dispatch("随便说点什么", "C002", "U002")

        # Some message must be sent (fallback or otherwise)
        assert mock_client.chat_postMessage.called or mock_client.chat_update.called


class TestContextIsolation:
    """Test that different channels have independent context."""

    @patch("slack_bot.dispatcher.WebClient")
    @patch("slack_bot.dispatcher.GeminiLLM")
    @patch("slack_bot.dispatcher.ContextStorage")
    def test_different_channels_get_separate_storage(
        self, mock_storage_cls: MagicMock, mock_llm_cls: MagicMock, mock_client_cls: MagicMock
    ) -> None:
        """Each channel creates its own ContextStorage instance."""
        from slack_bot.dispatcher import MessageDispatcher

        mock_llm = MagicMock()
        mock_llm_cls.return_value = mock_llm
        mock_llm.generate_response.return_value = ("OK", None)

        mock_storage = MagicMock()
        mock_storage_cls.return_value = mock_storage
        mock_storage.get_context.return_value = []

        mock_client_cls.return_value = MagicMock()

        dispatcher = MessageDispatcher()
        dispatcher.dispatch("Hello", "CHANNEL_A", "U001")
        dispatcher.dispatch("Hello", "CHANNEL_B", "U002")

        # ContextStorage should be instantiated with different channel IDs
        channel_ids = [call_args[0][0] for call_args in mock_storage_cls.call_args_list]
        assert "CHANNEL_A" in channel_ids
        assert "CHANNEL_B" in channel_ids
