"""Reaction (tapback) tests for PhotonAdapter.

Outbound reactions go through the sidecar's ``/react`` / ``/unreact``
endpoints; these tests stub ``_sidecar_call`` to assert endpoint + body
shape. Inbound reaction events are fed straight to ``_dispatch_inbound``.
Neither path spawns the Node sidecar or binds ports.
"""
from __future__ import annotations

from datetime import datetime, timezone
from typing import Any, Dict, List, Tuple

import pytest

from gateway.config import PlatformConfig
from gateway.platforms.base import MessageEvent, MessageType, ProcessingOutcome
from plugins.platforms.photon.adapter import PhotonAdapter

_EYES = "\U0001f440"
_THUMBS_UP = "\U0001f44d"
_THUMBS_DOWN = "\U0001f44e"


def _make_adapter(monkeypatch: pytest.MonkeyPatch) -> PhotonAdapter:
    monkeypatch.setenv("PHOTON_PROJECT_ID", "test-project-id")
    monkeypatch.setenv("PHOTON_PROJECT_SECRET", "test-project-secret")
    cfg = PlatformConfig(enabled=True, token="", extra={})
    return PhotonAdapter(cfg)


def _capture_sidecar(adapter: PhotonAdapter) -> List[Tuple[str, Dict[str, Any]]]:
    calls: List[Tuple[str, Dict[str, Any]]] = []

    async def _fake_call(path: str, body: Dict[str, Any]) -> Dict[str, Any]:
        calls.append((path, body))
        return {"ok": True, "messageId": "msg-123", "reactionId": "react-1"}

    adapter._sidecar_call = _fake_call  # type: ignore[assignment]
    return calls


def _capture_handled(
    adapter: PhotonAdapter, monkeypatch: pytest.MonkeyPatch
) -> List[MessageEvent]:
    captured: List[MessageEvent] = []

    async def fake_handle(event: MessageEvent) -> None:
        captured.append(event)

    monkeypatch.setattr(adapter, "handle_message", fake_handle)
    return captured


def _message_event(adapter: PhotonAdapter) -> MessageEvent:
    return MessageEvent(
        text="hi",
        message_type=MessageType.TEXT,
        source=adapter.build_source(
            chat_id="+15551234567",
            chat_name="+15551234567",
            chat_type="dm",
            user_id="+15551234567",
            user_name=None,
        ),
        message_id="target-msg-1",
        timestamp=datetime.now(tz=timezone.utc),
    )


def _reaction_event(
    emoji: str = "❤️",
    target_id: str = "bot-msg-1",
    target_direction: Any = "outbound",
    space_type: str = "dm",
    target_text: Any = "the bot's earlier reply",
) -> Dict[str, Any]:
    return {
        "messageId": "reaction-evt-1",
        "platform": "iMessage",
        "space": {"id": "+15551234567", "type": space_type, "phone": "+15551234567"},
        "sender": {"id": "+15551234567"},
        "content": {
            "type": "reaction",
            "emoji": emoji,
            "targetMessageId": target_id,
            "targetDirection": target_direction,
            # The sidecar always emits this key (hydrated reaction target);
            # null when the reacted-to message carried no text.
            "targetText": target_text,
        },
        "timestamp": "2026-06-11T10:00:00.000Z",
    }


# -- Outbound: /react and /unreact body shapes ------------------------------

@pytest.mark.asyncio
async def test_add_reaction_posts_react(monkeypatch: pytest.MonkeyPatch) -> None:
    adapter = _make_adapter(monkeypatch)
    calls = _capture_sidecar(adapter)

    ok = await adapter._add_reaction("+15551234567", "target-msg-1", _EYES)

    assert ok is True
    assert calls == [
        (
            "/react",
            {
                "spaceId": "+15551234567",
                "messageId": "target-msg-1",
                "emoji": _EYES,
            },
        )
    ]


@pytest.mark.asyncio
async def test_remove_reaction_posts_unreact(monkeypatch: pytest.MonkeyPatch) -> None:
    adapter = _make_adapter(monkeypatch)
    calls = _capture_sidecar(adapter)

    ok = await adapter._remove_reaction("+15551234567", "target-msg-1")

    assert ok is True
    assert calls == [
        ("/unreact", {"spaceId": "+15551234567", "messageId": "target-msg-1"})
    ]


@pytest.mark.asyncio
async def test_reaction_failure_is_soft(monkeypatch: pytest.MonkeyPatch) -> None:
    adapter = _make_adapter(monkeypatch)

    async def _boom(path: str, body: Dict[str, Any]) -> Dict[str, Any]:
        raise RuntimeError("sidecar down")

    adapter._sidecar_call = _boom  # type: ignore[assignment]

    assert await adapter._add_reaction("+1", "m", _EYES) is False
    assert await adapter._remove_reaction("+1", "m") is False


# -- Lifecycle hooks ---------------------------------------------------------

@pytest.mark.asyncio
async def test_hooks_noop_when_disabled(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.delenv("PHOTON_REACTIONS", raising=False)
    adapter = _make_adapter(monkeypatch)
    calls = _capture_sidecar(adapter)

    event = _message_event(adapter)
    await adapter.on_processing_start(event)
    await adapter.on_processing_complete(event, ProcessingOutcome.SUCCESS)

    assert calls == []


@pytest.mark.asyncio
async def test_processing_start_adds_eyes(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("PHOTON_REACTIONS", "true")
    adapter = _make_adapter(monkeypatch)
    calls = _capture_sidecar(adapter)

    await adapter.on_processing_start(_message_event(adapter))

    assert len(calls) == 1
    path, body = calls[0]
    assert path == "/react"
    assert body["emoji"] == _EYES
    assert body["messageId"] == "target-msg-1"


# -- Inbound reaction routing ------------------------------------------------

@pytest.mark.asyncio
async def test_inbound_reaction_on_bot_message_routed(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    adapter = _make_adapter(monkeypatch)
    captured = _capture_handled(adapter, monkeypatch)

    await adapter._dispatch_inbound(_reaction_event(emoji="❤️"))

    assert len(captured) == 1
    event = captured[0]
    assert event.text == "reaction:added:❤️"
    assert event.message_type == MessageType.TEXT
    assert event.source.chat_id == "+15551234567"
    # The tapback correlates to the bot message it reacted to, so the gateway
    # can inject `[Replying to your previous message: "..."]` for context.
    assert event.reply_to_message_id == "bot-msg-1"
    assert event.reply_to_text == "the bot's earlier reply"
    assert event.reply_to_is_own_message is True


