"""
Integration tests for rate limiting in the Slack event flow (CHAN-05).

Tests verify:
  1. Over-limit Slack events result in an ephemeral "too many requests" message
     being posted via the Slack client
  2. Over-limit events do NOT dispatch to Celery

These tests exercise the full gateway handler code path with:
  - fakeredis for rate limit state
  - mocked Slack client (no real Slack workspace)
  - mocked Celery task (no real Celery broker)
"""

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, check_rate_limit


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


def _make_slack_event(user_id: str = "U12345", channel: str = "C99999") -> dict:
    """Minimal Slack app_mention event payload."""
    return {
        "type": "app_mention",
        "user": user_id,
        "text": "<@UBOT123> hello",
        "ts": "1711234567.000100",
        "channel": channel,
        "channel_type": "channel",
        "_workspace_id": "T-WORKSPACE-X",
        "_bot_user_id": "UBOT123",
    }


def _make_mock_session_factory(tenant_id: str = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"):
    """Build a mock session factory that returns the given tenant_id."""
    mock_session = AsyncMock()
    mock_session.__aenter__ = AsyncMock(return_value=mock_session)
    mock_session.__aexit__ = AsyncMock(return_value=False)

    # resolve_tenant will call session methods — we patch at the function level
    mock_factory = MagicMock()
    mock_factory.return_value = mock_session
    return mock_factory


class TestRateLimitIntegration:
    """CHAN-05: Integration tests for rate limit behavior in Slack handler."""

    async def test_over_limit_sends_ephemeral_rejection(self, fake_redis) -> None:
        """
        When rate limit is exceeded, an ephemeral 'too many requests' message
        must be posted to the user, not dispatched to Celery.
        """
        tenant_id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
        mock_client = AsyncMock()
        mock_client.chat_postEphemeral = AsyncMock(return_value={"ok": True})
        mock_client.chat_postMessage = AsyncMock(return_value={"ts": "999.000", "ok": True})
        mock_say = AsyncMock()

        # Exhaust the rate limit
        for _ in range(30):
            await check_rate_limit(tenant_id, "slack", fake_redis, limit=30)

        event = _make_slack_event()

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

        # Ephemeral rejection must be sent
        mock_client.chat_postEphemeral.assert_called_once()
        call_kwargs = mock_client.chat_postEphemeral.call_args
        text = call_kwargs.kwargs.get("text", "")
        assert "too many requests" in text.lower() or "please try again" in text.lower()

        # Celery task must NOT be dispatched
        mock_celery_task.delay.assert_not_called()

    async def test_over_limit_does_not_post_placeholder(self, fake_redis) -> None:
        """
        When rate limited, no 'Thinking...' placeholder message should be posted
        (the request is rejected before reaching the placeholder step).
        """
        tenant_id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
        mock_client = AsyncMock()
        mock_client.chat_postEphemeral = AsyncMock(return_value={"ok": True})
        mock_client.chat_postMessage = AsyncMock(return_value={"ts": "999.000", "ok": True})
        mock_say = AsyncMock()

        # Exhaust the rate limit
        for _ in range(30):
            await check_rate_limit(tenant_id, "slack", fake_redis, limit=30)

        event = _make_slack_event()

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

        # Placeholder message must NOT be posted
        mock_client.chat_postMessage.assert_not_called()

    async def test_within_limit_dispatches_to_celery(self, fake_redis) -> None:
        """
        Requests within the rate limit must dispatch to Celery (not rejected).
        """
        tenant_id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
        mock_client = AsyncMock()
        mock_client.chat_postMessage = AsyncMock(return_value={"ts": "999.001", "ok": True})
        mock_say = AsyncMock()

        event = _make_slack_event()

        mock_task = MagicMock()
        mock_task.delay = MagicMock()

        with (
            patch("gateway.channels.slack.resolve_tenant", new_callable=AsyncMock, return_value=tenant_id),
            patch("router.idempotency.is_duplicate", new_callable=AsyncMock, return_value=False),
            patch("gateway.channels.slack._mark_thread_engaged", new_callable=AsyncMock),
            patch("gateway.channels.slack.handle_message_task", mock_task, create=True),
        ):
            # Patch the import inside the function
            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(tenant_id),
                    event_type="app_mention",
                )

        # Ephemeral rejection must NOT be sent
        mock_client.chat_postEphemeral.assert_not_called()

    async def test_ephemeral_message_includes_retry_hint(self, fake_redis) -> None:
        """
        The ephemeral rate limit rejection must mention when to retry.
        """
        tenant_id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
        mock_client = AsyncMock()
        mock_client.chat_postEphemeral = AsyncMock(return_value={"ok": True})
        mock_say = AsyncMock()

        # Exhaust the rate limit
        for _ in range(30):
            await check_rate_limit(tenant_id, "slack", fake_redis, limit=30)

        event = _make_slack_event()

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

        call_kwargs = mock_client.chat_postEphemeral.call_args
        text = call_kwargs.kwargs.get("text", "")
        # Message should give actionable guidance ("try again", "seconds", etc.)
        assert any(word in text.lower() for word in ["again", "second", "moment"])
