from __future__ import annotations

import asyncio
import tempfile
import unittest
from dataclasses import dataclass
from pathlib import Path
from types import SimpleNamespace

from memnixa.channels.base import BaseChannel, InboundChannelMessage
from memnixa.channels.feishu import FeishuChannel
from memnixa.channels.manager import ChannelManager
from memnixa.config import AppConfig
from memnixa.models import RunTurnResult
from memnixa.session import SessionStore


@dataclass
class FakeRuntime:
    config: AppConfig

    def __post_init__(self) -> None:
        self.calls: list[str] = []
        self.session = SessionStore(self.config.storage_path)

    def run_turn(self, inbound, *, event_callback=None) -> RunTurnResult:
        self.calls.append(inbound.session_id)
        return RunTurnResult(reply=f"echo:{inbound.body_for_agent}", iterations=1)


class FakeChannel(BaseChannel):
    name = "feishu"

    def __init__(self, config, inbound_handler) -> None:
        super().__init__(config, inbound_handler)
        self.sent: list[dict] = []
        self.indicator_events: list[tuple[str, str]] = []

    @property
    def capabilities(self) -> tuple[str, ...]:
        return ("reply_status",)

    async def start(self) -> None:
        self._running = True
        while self._running:
            await asyncio.sleep(0.1)

    async def stop(self) -> None:
        self._running = False

    async def send_text(
        self,
        reply_target: str,
        content: str,
        *,
        reply_target_type: str,
        metadata: dict | None = None,
    ) -> None:
        self.sent.append(
            {
                "reply_target": reply_target,
                "reply_target_type": reply_target_type,
                "content": content,
                "metadata": metadata or {},
            }
        )

    async def start_replying_indicator(self, message: InboundChannelMessage) -> None:
        await super().start_replying_indicator(message)
        self.indicator_events.append(("start", message.session_id))

    async def stop_replying_indicator(self, message: InboundChannelMessage) -> None:
        await super().stop_replying_indicator(message)
        self.indicator_events.append(("stop", message.session_id))


class ChannelManagerTests(unittest.TestCase):
    def test_channel_message_routes_through_runtime_and_back(self) -> None:
        with tempfile.TemporaryDirectory() as temp_dir:
            config = AppConfig.from_mapping(
                {
                    "default_provider": "1",
                    "providers": [
                        {
                            "id": "1",
                            "preset": "openai_chatgpt",
                            "api_key": "test-key",
                        }
                    ],
                    "workspace": temp_dir,
                    "database_path": str(Path(temp_dir) / ".memnixa/memnixa.db"),
                    "channels": {
                        "feishu": {
                            "enabled": True,
                            "app_id": "app-id",
                            "app_secret": "secret",
                        }
                    },
                }
            )
            runtime = FakeRuntime(config=config)
            manager = ChannelManager(
                runtime,
                config.channels,
                channel_factories={"feishu": FakeChannel},
            )
            message = InboundChannelMessage(
                channel="feishu",
                chat_id="oc_demo",
                sender_id="ou_demo",
                body="hello",
                reply_target="oc_demo",
                reply_target_type="chat_id",
                message_id="msg-1",
            )

            asyncio.run(manager.handle_inbound(message))

            state = runtime.session.get_session_state("feishu:oc_demo")

        self.assertEqual(runtime.calls, ["feishu:oc_demo"])
        channel = manager.channels["feishu"]
        self.assertEqual(channel.sent[0]["content"], "echo:hello")
        self.assertEqual(channel.sent[0]["reply_target"], "oc_demo")
        self.assertEqual(
            channel.indicator_events,
            [("start", "feishu:oc_demo"), ("stop", "feishu:oc_demo")],
        )
        self.assertEqual(channel.replying_sessions, ())
        self.assertIsNotNone(state)
        self.assertEqual(state.metadata["runtime_status"], "idle")
        self.assertEqual(state.metadata["channel"], "feishu")
        self.assertEqual(state.metadata["channel_capabilities"], ["reply_status"])
        self.assertEqual(state.metadata["source_mode"], "channel")

    def test_channel_status_reports_replying_state(self) -> None:
        with tempfile.TemporaryDirectory() as temp_dir:
            config = AppConfig.from_mapping(
                {
                    "default_provider": "1",
                    "providers": [
                        {
                            "id": "1",
                            "preset": "openai_chatgpt",
                            "api_key": "test-key",
                        }
                    ],
                    "workspace": temp_dir,
                    "database_path": str(Path(temp_dir) / ".memnixa/memnixa.db"),
                    "channels": {
                        "feishu": {
                            "enabled": True,
                            "app_id": "app-id",
                            "app_secret": "secret",
                        }
                    },
                }
            )
            runtime = FakeRuntime(config=config)
            manager = ChannelManager(
                runtime,
                config.channels,
                channel_factories={"feishu": FakeChannel},
            )

            status = manager.get_status()

        self.assertEqual(status["feishu"]["replying_count"], 0)
        self.assertEqual(status["feishu"]["replying_sessions"], [])

    def test_bound_owner_direct_message_routes_to_main_session(self) -> None:
        with tempfile.TemporaryDirectory() as temp_dir:
            config = AppConfig.from_mapping(
                {
                    "default_provider": "1",
                    "providers": [
                        {
                            "id": "1",
                            "preset": "openai_chatgpt",
                            "api_key": "test-key",
                        }
                    ],
                    "workspace": temp_dir,
                    "database_path": str(Path(temp_dir) / ".memnixa/memnixa.db"),
                    "channels": {
                        "feishu": {
                            "enabled": True,
                            "app_id": "app-id",
                            "app_secret": "secret",
                        }
                    },
                }
            )
            runtime = FakeRuntime(config=config)
            runtime.session.bind_identity(
                channel="feishu",
                external_id="ou_owner",
                user_id="owner",
                role="owner",
            )
            manager = ChannelManager(
                runtime,
                config.channels,
                channel_factories={"feishu": FakeChannel},
            )
            message = InboundChannelMessage(
                channel="feishu",
                chat_id="oc_direct_chat",
                sender_id="ou_owner",
                body="hello owner",
                reply_target="ou_owner",
                reply_target_type="open_id",
                message_id="msg-owner-1",
                metadata={"chat_type": "p2p"},
            )

            asyncio.run(manager.handle_inbound(message))

            state = runtime.session.get_session_state("main")

        self.assertEqual(runtime.calls, ["main"])
        channel = manager.channels["feishu"]
        self.assertEqual(
            channel.indicator_events,
            [("start", "main"), ("stop", "main")],
        )
        self.assertIsNotNone(state)
        self.assertEqual(state.metadata["actor_identity_status"], "owner")
        self.assertEqual(state.metadata["actor_role"], "owner")
        self.assertEqual(state.metadata["actor_user_id"], "owner")
        self.assertTrue(state.metadata["actor_is_owner"])


class _FakeFeishuMessageReactionAPI:
    def __init__(self) -> None:
        self.created: list[str] = []
        self.deleted: list[tuple[str, str]] = []

    def create(self, request) -> SimpleNamespace:
        message_id = str(getattr(request, "message_id", ""))
        self.created.append(message_id)
        return SimpleNamespace(
            success=lambda: True,
            data=SimpleNamespace(reaction_id=f"reaction:{message_id}"),
        )

    def delete(self, request) -> SimpleNamespace:
        self.deleted.append((str(request.message_id), str(request.reaction_id)))
        return SimpleNamespace(success=lambda: True)


class FeishuChannelReplyReactionTests(unittest.TestCase):
    def test_feishu_reply_indicator_creates_and_deletes_reaction(self) -> None:
        config = AppConfig.from_mapping(
            {
                "default_provider": "1",
                "providers": [
                    {
                        "id": "1",
                        "preset": "openai_chatgpt",
                        "api_key": "test-key",
                    }
                ],
                "channels": {
                    "feishu": {
                        "enabled": True,
                        "app_id": "app-id",
                        "app_secret": "secret",
                    }
                },
            }
        )
        reaction_api = _FakeFeishuMessageReactionAPI()
        channel = FeishuChannel(
            config.channels.feishu, lambda message: asyncio.sleep(0)
        )
        channel._client = SimpleNamespace(
            im=SimpleNamespace(v1=SimpleNamespace(message_reaction=reaction_api))
        )
        message = InboundChannelMessage(
            channel="feishu",
            chat_id="oc_demo",
            sender_id="ou_demo",
            body="hello",
            reply_target="oc_demo",
            reply_target_type="chat_id",
            message_id="msg-1",
        )

        async def scenario() -> None:
            await channel.start_replying_indicator(message)
            await channel.start_replying_indicator(message)
            await channel.stop_replying_indicator(message)

        asyncio.run(scenario())

        self.assertEqual(channel.capabilities, ("reply_status", "message_reactions"))
        self.assertEqual(reaction_api.created, ["msg-1"])
        self.assertEqual(reaction_api.deleted, [("msg-1", "reaction:msg-1")])
        self.assertEqual(channel.replying_sessions, ())
