"""
Integration tests for the end-to-end Slack event flow (CHAN-02).

Tests verify:
  1. app_mention event -> normalize -> tenant resolve -> Celery dispatch -> LLM -> thread reply
  2. DM (message with channel_type="im") follows the same pipeline
  3. Placeholder "Thinking..." is posted before Celery dispatch
  4. Placeholder is replaced with real response (via chat.update in orchestrator task)
  5. Bot messages are ignored (no infinite response loop)
  6. Unknown workspace_id events are silently ignored

All tests mock the Slack client and Celery task — no live Slack workspace or
Celery broker required.
"""

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock, patch

import fakeredis
import pytest
import pytest_asyncio

from gateway.channels.slack import _handle_slack_event
from gateway.normalize import normalize_slack_event
from shared.models.message import ChannelType


@pytest_asyncio.fixture
async def fake_redis():
    """Fake async Redis."""
    r = fakeredis.aioredis.FakeRedis(decode_responses=True)
    yield r
    await r.aclose()


TENANT_ID = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
WORKSPACE_ID = "T-WORKSPACE-TEST"


def _make_mention_event(
    user: str = "U12345",
    text: str = "<@UBOT123> can you help me?",
    channel: str = "C99999",
) -> dict:
    """Build a Slack app_mention event."""
    return {
        "type": "app_mention",
        "user": user,
        "text": text,
        "ts": "1711234567.000100",
        "channel": channel,
        "channel_type": "channel",
        "_workspace_id": WORKSPACE_ID,
        "_bot_user_id": "UBOT123",
    }


def _make_dm_event(
    user: str = "U12345",
    text: str = "help me please",
    channel: str = "D11111",
) -> dict:
    """Build a Slack DM (im) event."""
    return {
        "type": "message",
        "user": user,
        "text": text,
        "ts": "1711234567.000200",
        "channel": channel,
        "channel_type": "im",
        "_workspace_id": WORKSPACE_ID,
        "_bot_user_id": "UBOT123",
    }


def _make_bot_event() -> dict:
    """A Slack bot_message event (must be ignored)."""
    return {
        "type": "message",
        "bot_id": "B11111",
        "subtype": "bot_message",
        "text": "I replied to something",
        "ts": "1711234567.000300",
        "channel": "C99999",
        "_workspace_id": WORKSPACE_ID,
        "_bot_user_id": "UBOT123",
    }


class TestNormalization:
    """normalize_slack_event unit coverage for CHAN-02 gateway code path."""

    def test_mention_text_strips_bot_token(self) -> None:
        """Bot @mention token must be stripped from text before sending to agent."""
        event = {"user": "U1", "text": "<@UBOT123> hello there", "ts": "123.456"}
        msg = normalize_slack_event(event, workspace_id="T-WS", bot_user_id="UBOT123")
        assert msg.content.text == "hello there"
        assert "<@UBOT123>" not in msg.content.text

    def test_channel_type_is_slack(self) -> None:
        """Normalized message must have channel=SLACK."""
        event = {"user": "U1", "text": "hi", "ts": "123.456"}
        msg = normalize_slack_event(event, workspace_id="T-WS")
        assert msg.channel == ChannelType.SLACK

    def test_tenant_id_none_after_normalization(self) -> None:
        """tenant_id must be None — Router populates it, not the normalizer."""
        event = {"user": "U1", "text": "hi", "ts": "123.456"}
        msg = normalize_slack_event(event, workspace_id="T-WS")
        assert msg.tenant_id is None

    def test_thread_id_set_from_thread_ts(self) -> None:
        """thread_id must be set from thread_ts when present."""
        event = {
            "user": "U1",
            "text": "reply",
            "ts": "123.999",
            "thread_ts": "123.000",
        }
        msg = normalize_slack_event(event, workspace_id="T-WS")
        assert msg.thread_id == "123.000"

    def test_workspace_id_in_channel_metadata(self) -> None:
        """workspace_id must be stored in channel_metadata."""
        event = {"user": "U1", "text": "hi", "ts": "123.456"}
        msg = normalize_slack_event(event, workspace_id="T-WORKSPACE-X")
        assert msg.channel_metadata["workspace_id"] == "T-WORKSPACE-X"


class TestSlackMentionFlow:
    """CHAN-02: End-to-end app_mention event pipeline."""

    async def test_mention_posts_thinking_placeholder(self, fake_redis) -> None:
        """
        When a valid @mention arrives, a 'Thinking...' placeholder must be
        posted in-thread before Celery dispatch.
        """
        mock_client = AsyncMock()
        mock_client.chat_postMessage = AsyncMock(return_value={"ts": "999.001", "ok": True})
        mock_say = AsyncMock()

        event = _make_mention_event()

        with (
            patch("gateway.channels.slack.resolve_tenant", new_callable=AsyncMock, return_value=TENANT_ID),
            patch("gateway.channels.slack.is_duplicate", new_callable=AsyncMock, return_value=False),
            patch("gateway.channels.slack._mark_thread_engaged", new_callable=AsyncMock),
            patch("orchestrator.tasks.handle_message") as celery_mock,
        ):
            celery_mock.delay = MagicMock()
            await _handle_slack_event(
                event=event,
                say=mock_say,
                client=mock_client,
                redis=fake_redis,
                get_session=_make_mock_session_factory(),
                event_type="app_mention",
            )

        # Placeholder must have been posted
        mock_client.chat_postMessage.assert_called_once()
        call_kwargs = mock_client.chat_postMessage.call_args
        placeholder_text = call_kwargs.kwargs.get("text", "")
        assert "thinking" in placeholder_text.lower()

    async def test_mention_posts_placeholder_in_thread(self, fake_redis) -> None:
        """
        The placeholder must be posted with thread_ts set (in-thread reply).
        """
        mock_client = AsyncMock()
        mock_client.chat_postMessage = AsyncMock(return_value={"ts": "999.001", "ok": True})
        mock_say = AsyncMock()

        event = _make_mention_event()
        event["thread_ts"] = "1711234567.000000"  # Set thread context

        with (
            patch("gateway.channels.slack.resolve_tenant", new_callable=AsyncMock, return_value=TENANT_ID),
            patch("gateway.channels.slack.is_duplicate", new_callable=AsyncMock, return_value=False),
            patch("gateway.channels.slack._mark_thread_engaged", new_callable=AsyncMock),
            patch("orchestrator.tasks.handle_message") as celery_mock,
        ):
            celery_mock.delay = MagicMock()
            await _handle_slack_event(
                event=event,
                say=mock_say,
                client=mock_client,
                redis=fake_redis,
                get_session=_make_mock_session_factory(),
                event_type="app_mention",
            )

        call_kwargs = mock_client.chat_postMessage.call_args
        # thread_ts must be set in the placeholder post
        assert call_kwargs.kwargs.get("thread_ts") is not None

    async def test_mention_dispatches_celery_task(self, fake_redis) -> None:
        """
        After the placeholder is posted, the Celery task must be dispatched
        with the message data.
        """
        mock_client = AsyncMock()
        mock_client.chat_postMessage = AsyncMock(return_value={"ts": "999.001", "ok": True})
        mock_say = AsyncMock()

        event = _make_mention_event()

        dispatched_payloads: list[dict] = []

        def capture_delay(payload: dict) -> MagicMock:
            dispatched_payloads.append(payload)
            return MagicMock()

        with (
            patch("gateway.channels.slack.resolve_tenant", new_callable=AsyncMock, return_value=TENANT_ID),
            patch("gateway.channels.slack.is_duplicate", new_callable=AsyncMock, return_value=False),
            patch("gateway.channels.slack._mark_thread_engaged", new_callable=AsyncMock),
            patch("orchestrator.tasks.handle_message") as celery_mock,
        ):
            celery_mock.delay = MagicMock(side_effect=capture_delay)
            await _handle_slack_event(
                event=event,
                say=mock_say,
                client=mock_client,
                redis=fake_redis,
                get_session=_make_mock_session_factory(),
                event_type="app_mention",
            )

        assert len(dispatched_payloads) == 1
        payload = dispatched_payloads[0]
        assert payload["tenant_id"] == TENANT_ID
        assert payload["channel"] == "slack"
        # placeholder_ts and channel_id must be present
        assert "placeholder_ts" in payload
        assert "channel_id" in payload

    async def test_celery_payload_has_placeholder_ts(self, fake_redis) -> None:
        """Celery payload must include placeholder_ts for post-LLM chat.update."""
        mock_client = AsyncMock()
        placeholder_ts = "1234567890.111111"
        mock_client.chat_postMessage = AsyncMock(
            return_value={"ts": placeholder_ts, "ok": True}
        )
        mock_say = AsyncMock()

        event = _make_mention_event()
        dispatched_payloads: list[dict] = []

        with (
            patch("gateway.channels.slack.resolve_tenant", new_callable=AsyncMock, return_value=TENANT_ID),
            patch("gateway.channels.slack.is_duplicate", new_callable=AsyncMock, return_value=False),
            patch("gateway.channels.slack._mark_thread_engaged", new_callable=AsyncMock),
            patch("orchestrator.tasks.handle_message") as celery_mock,
        ):
            celery_mock.delay = MagicMock(side_effect=lambda p: dispatched_payloads.append(p))
            await _handle_slack_event(
                event=event,
                say=mock_say,
                client=mock_client,
                redis=fake_redis,
                get_session=_make_mock_session_factory(),
                event_type="app_mention",
            )

        assert dispatched_payloads[0]["placeholder_ts"] == placeholder_ts


class TestDMFlow:
    """CHAN-02: Direct message event pipeline."""

    async def test_dm_triggers_same_pipeline(self, fake_redis) -> None:
        """
        A DM (channel_type='im') must trigger the same handler pipeline
        as an @mention.
        """
        mock_client = AsyncMock()
        mock_client.chat_postMessage = AsyncMock(return_value={"ts": "999.002", "ok": True})
        mock_say = AsyncMock()

        event = _make_dm_event()

        with (
            patch("gateway.channels.slack.resolve_tenant", new_callable=AsyncMock, return_value=TENANT_ID),
            patch("gateway.channels.slack.is_duplicate", new_callable=AsyncMock, return_value=False),
            patch("gateway.channels.slack._mark_thread_engaged", new_callable=AsyncMock),
            patch("orchestrator.tasks.handle_message") as celery_mock,
        ):
            celery_mock.delay = MagicMock()
            await _handle_slack_event(
                event=event,
                say=mock_say,
                client=mock_client,
                redis=fake_redis,
                get_session=_make_mock_session_factory(),
                event_type="dm",
            )

        # Pipeline should proceed: placeholder posted + Celery dispatched
        mock_client.chat_postMessage.assert_called_once()
        celery_mock.delay.assert_called_once()

    async def test_dm_payload_channel_is_slack(self, fake_redis) -> None:
        """DM normalized message must still have channel=slack."""
        mock_client = AsyncMock()
        mock_client.chat_postMessage = AsyncMock(return_value={"ts": "999.002", "ok": True})
        mock_say = AsyncMock()

        event = _make_dm_event()
        dispatched_payloads: list[dict] = []

        with (
            patch("gateway.channels.slack.resolve_tenant", new_callable=AsyncMock, return_value=TENANT_ID),
            patch("gateway.channels.slack.is_duplicate", new_callable=AsyncMock, return_value=False),
            patch("gateway.channels.slack._mark_thread_engaged", new_callable=AsyncMock),
            patch("orchestrator.tasks.handle_message") as celery_mock,
        ):
            celery_mock.delay = MagicMock(side_effect=lambda p: dispatched_payloads.append(p))
            await _handle_slack_event(
                event=event,
                say=mock_say,
                client=mock_client,
                redis=fake_redis,
                get_session=_make_mock_session_factory(),
                event_type="dm",
            )

        assert dispatched_payloads[0]["channel"] == "slack"


class TestBotIgnoring:
    """Verify bot messages are silently ignored to prevent infinite loops."""

    async def test_bot_message_is_ignored(self, fake_redis) -> None:
        """
        Events with bot_id must be silently dropped — no placeholder, no Celery.
        """
        mock_client = AsyncMock()
        mock_say = AsyncMock()

        event = _make_bot_event()

        with patch("orchestrator.tasks.handle_message") as celery_mock:
            celery_mock.delay = MagicMock()
            await _handle_slack_event(
                event=event,
                say=mock_say,
                client=mock_client,
                redis=fake_redis,
                get_session=_make_mock_session_factory(),
                event_type="dm",
            )

        # Nothing should happen
        mock_client.chat_postMessage.assert_not_called()
        celery_mock.delay.assert_not_called()

    async def test_bot_message_subtype_is_ignored(self, fake_redis) -> None:
        """Events with subtype=bot_message must also be ignored."""
        mock_client = AsyncMock()
        mock_say = AsyncMock()

        event = {
            "subtype": "bot_message",
            "text": "automated message",
            "ts": "123.456",
            "channel": "C99999",
            "_workspace_id": WORKSPACE_ID,
        }

        with patch("orchestrator.tasks.handle_message") as celery_mock:
            celery_mock.delay = MagicMock()
            await _handle_slack_event(
                event=event,
                say=mock_say,
                client=mock_client,
                redis=fake_redis,
                get_session=_make_mock_session_factory(),
                event_type="dm",
            )

        mock_client.chat_postMessage.assert_not_called()
        celery_mock.delay.assert_not_called()


class TestUnknownWorkspace:
    """Verify unknown workspace_id events are silently ignored."""

    async def test_unknown_workspace_silently_ignored(self, fake_redis) -> None:
        """
        If workspace_id maps to no tenant, the event must be silently dropped —
        no placeholder, no Celery dispatch, no exception raised.
        """
        mock_client = AsyncMock()
        mock_say = AsyncMock()

        event = _make_mention_event()

        with (
            patch("gateway.channels.slack.resolve_tenant", new_callable=AsyncMock, return_value=None),
            patch("orchestrator.tasks.handle_message") as celery_mock,
        ):
            celery_mock.delay = MagicMock()
            # Must not raise
            await _handle_slack_event(
                event=event,
                say=mock_say,
                client=mock_client,
                redis=fake_redis,
                get_session=_make_mock_session_factory(),
                event_type="app_mention",
            )

        mock_client.chat_postMessage.assert_not_called()
        celery_mock.delay.assert_not_called()


class TestIdempotency:
    """Verify duplicate Slack events (retries) are not double-dispatched."""

    async def test_duplicate_event_is_skipped(self, fake_redis) -> None:
        """
        If a message was already processed (Slack retry), no placeholder
        is posted and Celery is not called again.
        """
        mock_client = AsyncMock()
        mock_say = AsyncMock()

        event = _make_mention_event()

        with (
            patch("gateway.channels.slack.resolve_tenant", new_callable=AsyncMock, return_value=TENANT_ID),
            patch("gateway.channels.slack.is_duplicate", new_callable=AsyncMock, return_value=True),
            patch("orchestrator.tasks.handle_message") as celery_mock,
        ):
            celery_mock.delay = MagicMock()
            await _handle_slack_event(
                event=event,
                say=mock_say,
                client=mock_client,
                redis=fake_redis,
                get_session=_make_mock_session_factory(),
                event_type="app_mention",
            )

        mock_client.chat_postMessage.assert_not_called()
        celery_mock.delay.assert_not_called()


# ---------------------------------------------------------------------------
# Helper
# ---------------------------------------------------------------------------

def _make_mock_session_factory(tenant_id: str = TENANT_ID) -> MagicMock:
    """Return a mock async context manager factory for DB sessions."""
    mock_session = AsyncMock()
    mock_session.__aenter__ = AsyncMock(return_value=mock_session)
    mock_session.__aexit__ = AsyncMock(return_value=False)
    mock_factory = MagicMock()
    mock_factory.return_value = mock_session
    return mock_factory
